后台任务

后台任务机制支持异步执行任务,不阻塞用户界面。

基本用法

如需使用后台任务,请按照下列步骤:

  1. 定义一个继承自 BackgroundTask 抽象类的任务。任务类的构造函数接收两个参数,一个是任务关联的界面控制器,另一个是用于任务构造函数的超时时限。

    关闭界面将中断与其相关的任务。此外,任务将在指定的时限超时后自动中断。

  2. BackgroundTask.run() 方法中实现任务逻辑。

  3. 通过将任务实例传递给 BackgroundWorker bean 的 handle() 方法,可以创建一个 BackgroundTaskHandler 类对象用于对任务进行控制。在界面控制器中注入或通过 ApplicationContext 类来获得对 BackgroundWorker 的引用。

  4. 调用 BackgroundTaskHandlerexecute() 方法来运行任务。

不要在 BackgroundTask.run() 方法中读取或更新 UI 组件的状态和数据容器,可以使用 done()progress()canceled() 回调方法替代。如果尝试从后台线程设置 UI 组件的值,则会抛出 IllegalConcurrentAccessException

下面示例中,运行一个后台任务并通过 ProgressBar 组件跟踪任务进度:

@Autowired
protected ProgressBar progressBar;
@Autowired
protected BackgroundWorker backgroundWorker;

private static final int ITERATIONS = 6;

@Subscribe
protected void onInit(InitEvent event) {
    BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(100) {
        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            for (int i = 1; i <= ITERATIONS; i++) {
                TimeUnit.SECONDS.sleep(1); (1)
                taskLifeCycle.publish(i);
            }
            return null;
        }

        @Override
        public void progress(List<Integer> changes) {
            double lastValue = changes.get(changes.size() - 1);
            progressBar.setValue(lastValue / ITERATIONS); (2)
        }
    };

    BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
    taskHandler.execute();
}
1 需要一些时间才能完成的任务。run() 方法在单独线程中执行。
2 progress() 方法在 UI 线程中执行,因此我们可以在这里更新可视化组件状态。

BackgroundTask 类

BackgroundTask<T, V> 是一个参数类:

  1. T - 显示任务进度的对象类型。在工作线程中调用 TaskLifeCycle.publish() 期间,将此类型对象传递给任务的 progress() 方法。

  2. V - 传递给 done() 方法的任务结果类型。结果也可以调用 BackgroundTaskHandler.getResult() 方法获得,该方法将等待至任务完成。

BackgroundTask 对象是无状态的。如果在实现任务类时没有为临时数据创建字段,则可以使用单个任务实例启动多个并行进程。

BackgroundTask 类方法

JavaDocs 中提供了 BackgroundTaskTaskLifeCycleBackgroundTaskHandler 类的有关方法的详细信息。

run()

该方法在单独的工作线程中调用,用于执行一个任务。

该方法应该支持外部中断。如需提供该功能,建议在长时间运行时定期检查 TaskLifeCycle.isInterrupted() 标识,并在需要时停止执行。另外,不应该忽略掉 InterruptedException(或任何其他异常)- 而应该正确退出方法或根本不处理异常(将异常暴露给调用方)

还可以用 isCancelled() 方法检查任务是否是通过 cancel() 方法中断。

canceled()

仅当主动取消任务时在 UI 线程中调用该方法,例如,在 TaskHandler 中调用 cancel() 方法时。

progress()

当进度值发生变化时在 UI 线程中调用该方法,例如,调用 taskLifeCycle.publish() 方法之后。

done()

当任务完成后在 UI 线程中调用该方法。

handleTimeoutException()

当任务超时后在 UI 线程中调用该方法。如果正在运行任务的窗口关闭,则任务将在没有通知的情况下停止。

handleException()

当发生任何异常时在 UI 线程中调用该方法。

注意事项

  1. dialogs.createBackgroundWorkDialog() 方法展示模态窗,包含一个进度条和 Cancel 按钮。该模态窗可以定义进度条的不同类型,以及允许或禁止取消后台任务。

  2. 如果需要在任务线程中使用可视化组件的某个值,应该在 getParams() 方法中实现获取的逻辑,该方法在任务启动时在 UI 线程中运行一次。在 run() 方法中,可以通过 TaskLifeCycle 对象的 getParams() 方法访问这些参数。

  3. 后台任务受 jmix.ui.background-task-timeout-check-intervaljmix.ui.background-task.threads-count 应用程序属性的影响。

用例

通常,在启动后台任务时,你需要展示一个简单的用户界面:

  1. 为用户展示请求的操作正在执行中。

  2. 允许用户中止长时间的操作。

  3. 如果可以确定执行的百分比,则显示操作的进度。

可以用 Dialogs 接口的 createBackgroundWorkDialog() 方法完成。

考虑下面情形作为示例:

  1. 给定的界面包含展示 customer 的表格,支持多选。

  2. 当用户点击按钮时,系统给选定的 customer 发送提醒邮件,发邮件的过程不阻塞 UI,并支持中止操作。

emails

@Autowired
private Table<Customer> customersTable;
@Autowired
private Emailer emailer;
@Autowired
private Dialogs dialogs;

@Subscribe("sendByEmail")
public void onSendByEmailClick(Button.ClickEvent event) {
    Set<Customer> selected = customersTable.getSelected();
    if (selected.isEmpty()) {
        return;
    }
    BackgroundTask<Integer, Void> task = new EmailTask(selected);
    dialogs.createBackgroundWorkDialog(this, task) (1)
            .withCaption("Sending reminder emails")
            .withMessage("Please wait while emails are being sent")
            .withTotal(selected.size())
            .withShowProgressInPercentage(true)
            .withCancelAllowed(true)
            .show();
}

private class EmailTask extends BackgroundTask<Integer, Void> { (2)
    private Set<Customer> customers; (3)

    public EmailTask(Set<Customer> customers) {
        super(10, TimeUnit.MINUTES, BackgroundTasksScreen.this); (4)
        this.customers = customers;
    }

    @Override
    public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
        int i = 0;
        for (Customer customer : customers) {
            if (taskLifeCycle.isCancelled()) { (5)
                break;
            }
            EmailInfo emailInfo = EmailInfoBuilder.create() (6)
                    .setAddresses(customer.getEmail())
                    .setSubject("Reminder")
                    .setBody("Your password expires in 14 days!")
                    .build();
            emailer.sendEmail(emailInfo);

            i++;
            taskLifeCycle.publish(i); (7)
        }
        return null;
    }
}
1 启动任务,展示进度对话框,并设置:
  • 对话框标题;

  • 对话框信息;

  • 任务进度的总数;

  • 是否展示进度百分比;

  • 是否展示 Cancel 按钮。

2 任务进度单位是 Integer - 处理的表格条目数量,结果类型是 Void,因为任务不会返回结果。
3 选中的表格条目保存在一个变量中,会在任务的构造函数中初始化。必须如此实现,因为 run() 会在后台线程中执行,无法访问 UI 组件。
4 设置超时时限为 10 分钟。
5 周期性的检查 isCancelled(),以便在用户点击 Cancel 按钮时立即停止任务。
6 发送邮件。参阅 文档 了解关于发送邮件的更多内容。
7 每个邮件发送后,更新进度条位置。