跳到主要内容

Background Jobs

在ChatGPT中打开

当用户单击按钮以生成报告或处理数据时,他们期望界面保持响应。进度条应该动画,按钮应该对悬停做出反应,应用程序不应该冻结。Spring 的 @Async 注解使这成为可能,它将长时间运行的操作移动到后台线程。

webforJ 强调 UI 组件的线程安全——所有更新必须在 UI 线程上发生。这带来了一个挑战:后台任务如何更新进度条或显示结果?答案是 Environment.runLater(),它安全地将 UI 更新从 Spring 的后台线程转移到 webforJ 的 UI 线程。

启用异步执行

Spring 的异步方法执行需要显式配置。没有它,带有 @Async 注解的方法会同步执行,从而失去其目的。

在您的 Spring Boot 应用程序类中添加 @EnableAsync

@SpringBootApplication
@EnableAsync
@Routify(packages = { "com.example.views" })
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

@EnableAsync 注解激活 Spring 的基础设施,以检测 @Async 方法并在后台线程上执行它们。

Spring 异步指南

有关 Spring 的 @Async 注解和基本用法模式的简要介绍,请参见 创建异步方法

创建异步服务

带有 @Service 注解的服务可以标记为使用 @Async 声明在后台线程中运行的方法。这些方法通常返回 CompletableFuture 以便进行适当的完成处理和取消:

@Service
public class BackgroundService {

@Async
public CompletableFuture<String> performLongRunningTask(Consumer<Integer> progressCallback) {
try {
for (int i = 0; i <= 10; i++) {
// 报告进度
int progress = i * 10;
if (progressCallback != null) {
progressCallback.accept(progress);
}

// 模拟工作
Thread.sleep(500);
}

return CompletableFuture.completedFuture(
"任务成功完成来自后台服务!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return CompletableFuture.failedFuture(e);
}
}
}

该服务接受一个进度回调(Consumer<Integer>),该回调将在后台线程中被调用。回调模式允许服务报告进度,而无需了解 UI 组件。

该方法模拟了一个 5 秒的任务,并进行了 10 次进度更新。在生产中,这将是实际的工作,比如数据库查询或文件处理。异常处理恢复中断状态,以支持在调用 cancel(true) 时进行适当的任务取消。

在视图中使用后台任务

视图通过构造函数注入接收后台服务:

@Route("/")
public class HelloWorldView extends Composite<FlexLayout> {
private Button asyncBtn = new Button("启动后台任务");
private ProgressBar progressBar = new ProgressBar();
private CompletableFuture<String> currentTask;

public HelloWorldView(BackgroundService backgroundService) {
// 服务由 Spring 注入
asyncBtn.addClickListener(e -> {
currentTask = backgroundService.performLongRunningTask(progress -> {
Environment.runLater(() -> {
progressBar.setValue(progress);
});
});
});
}
}

Spring 将 BackgroundService 注入到视图的构造函数中,就像其他 Spring bean 一样。视图然后使用该服务启动后台任务。关键概念:服务中的回调在后台线程中执行,因此这些回调中的任何 UI 更新都必须使用 Environment.runLater() 将执行转移到 UI 线程。

完成处理需要相同的细致线程管理:

currentTask.whenComplete((result, error) -> {
Environment.runLater(() -> {
asyncBtn.setEnabled(true);
progressBar.setVisible(false);
if (error != null) {
Toast.show("任务失败: " + error.getMessage(), Theme.DANGER);
} else {
Toast.show(result, Theme.SUCCESS);
}
});
});

whenComplete 回调也在后台线程中执行。每个 UI 操作——启用按钮、隐藏进度条、显示吐司——都必须包装在 Environment.runLater() 中。如果没有这个包装,webforJ 将抛出异常,因为后台线程无法访问 UI 组件。

线程安全

从后台线程中的每个 UI 更新必须包装在 Environment.runLater() 中。这个规则没有例外。从 @Async 方法直接访问组件总是会失败。

了解更多关于线程安全

有关 webforJ 的线程模型、执行行为和哪些操作需要 Environment.runLater() 的详细信息,请参见 异步更新

任务取消和清理

适当的生命周期管理可以防止内存泄漏和不必要的 UI 更新。视图存储 CompletableFuture 引用:

private CompletableFuture<String> currentTask;

当视图被销毁时,取消任何正在运行的任务:

@Override
protected void onDestroy() {
// 如果视图被销毁,则取消任务
if (currentTask != null && !currentTask.isDone()) {
currentTask.cancel(true);
}
}

cancel(true) 参数是关键。它会中断后台线程,导致阻塞操作如 Thread.sleep() 抛出 InterruptedException。这使得任务能够立即终止。如果没有中断标志(cancel(false)),任务将继续运行,直到它显式检查取消。

这种清理可以防止几个问题:

  • 后台线程在视图消失后继续消耗资源
  • UI 更新试图修改已销毁的组件
  • 回调持有对 UI 组件的引用导致内存泄漏