Background Jobs
当用户点击按钮生成报告或处理数据时,他们期望界面能够保持响应。进度条应该动画化,按钮在悬停时应该反应,应用程序不应该冻结。Spring 的 @Async 注解使这一切成为可能,它将长时间运行的操作移至后台线程。
webforJ 强制执行 UI 组件的线程安全 - 所有更新必须在 UI 线程上发生。这带来了一个挑战:后台任务如何更新进度条或显示结果?答案是 Environment.runLater(),它安全地将 UI 更新从 Spring 的后台线程转移到 webforJ 的 UI 线程。
启用异步执行
Spring 的异步方法执行需要明确的配置。没有它,带有 @Async 注解的方法将会同步执行,从而失去其目的。
将 @EnableAsync 添加到您的 Spring Boot 应用程序类中:
@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 的 @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 组件的引用导致内存泄漏