后台任务

后台任务机制可以使用当前用户的安全上下文异步执行任务,并使用任务的执行结果更新 UI。

与更简单的 异步任务 相比,后台任务 API 提供了以下额外的功能:

  • 能够看到操作进度。

  • 用户能够中断操作。

  • 带有一个即用的可配置 UI 对话框,用于显示有关操作的信息、进度条和取消按钮。

基本用法

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

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

    关闭视图将中断与视图关联的后台任务。此外,任务将在指定的时限超时后自动中断。

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

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

  4. 调用 BackgroundTaskHandlerexecute() 方法启动任务。

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

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

@ViewComponent
protected ProgressBar progressBar; (1)
@Autowired
protected BackgroundWorker backgroundWorker;

protected BackgroundTaskHandler<Void> taskHandler;

private static final int ITERATIONS = 6;

@Subscribe
protected void onInit(InitEvent event) {  (2)
    taskHandler = backgroundWorker.handle(createBackgroundTask());
    taskHandler.execute();
}

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

        @Override
        public void progress (List<Integer> changes) {
            double lastValue = changes.get(changes.size() - 1);
            double value = lastValue/ITERATIONS;
            progressBar.setValue(value); (4)
        }
    };
}
1 需要一些时间才能完成的任务。run() 方法在单独线程中执行。
2 progress() 方法在 UI 线程中执行,因此我们可以在这里更新 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.createBackgroundTaskDialog() 方法展示一个包含进度条和 Cancel 按钮的模态窗。该模态窗可以定义进度条的不同类型,以及允许或禁止取消后台任务。

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

  3. 后台任务相关的配置参数有:jmix.ui.background-task.task-killing-latencyjmix.ui.background-task.threads-countjmix.ui.background-task.timeout-expiration-check-interval

用例

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

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

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

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

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

考虑下面情形作为示例:

  1. 视图中包含 users 的表格,支持多选。

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

emails

@ViewComponent
private DataGrid<User> usersTable;

@Autowired
private Emailer emailer;

@Autowired
private Dialogs dialogs;

@Subscribe("sendByEmail")
public void onSendByEmailClick(ClickEvent event) {
    Set<User> selected = usersTable.getSelectedItems();
    if (selected.isEmpty()) {
        return;
    }
    BackgroundTask<Integer, Void> task = new EmailTask(selected);
    dialogs.createBackgroundTaskDialog(task) (1)
            .withHeader("Sending reminder emails")
            .withText("Please wait while emails are being sent")
            .withTotal(selected.size())
            .withShowProgressInPercentage(true)
            .withCancelAllowed(true)
            .open();
}

private class EmailTask extends BackgroundTask<Integer, Void> { (2)

    private Set<User> users; (3)

    public EmailTask(Set<User> users) {
        super(10, TimeUnit.MINUTES, BackgroundTasksView.this); (4)
        this.users = users;
    }

    @Override
    public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
        int i = 0;
        for (User user : users) {
            if (taskLifeCycle.isCancelled()) { (5)
                break;
            }
            EmailInfo emailInfo = EmailInfoBuilder.create() (6)
                    .setAddresses(user.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 每个邮件发送后,更新进度条位置。