Фоновые задачи

Механизм фоновых задач предназначен для выполнения задач асинхронно без блокировки пользовательского интерфейса.

Основы

Для использования фоновых задач выполните следующие действия:

  1. Определите задачу как подкласс абстрактного класса BackgroundTask. В конструкторе вашей задачи передайте значение времени ожидания и ссылку на связанный с задачей контроллер экрана.

    Закрытие экрана прерывает связанные с ним задачи. Кроме того, задача прерывается автоматически после указанного времени ожидания.

  2. Реализуйте задачу в методе BackgroundTask.run().

  3. Создайте объект класса BackgroundTaskHandler, управляющий задачей, передав экземпляр задачи методу handle() бина BackgroundWorker. Ссылку на BackgroundWorker можно получить путем инжектирования в контроллер экрана или через ApplicationContext.

  4. Запустите задачу, вызвав метод execute() объекта BackgroundTaskHandler.

Не читайте и не обновляйте состояние компонентов пользовательского интерфейса и контейнеров данных в методе BackgroundTask.run(); вместо этого используйте методы обратного вызова done(), progress() и canceled(). Если вы попытаетесь установить значение компоненту пользовательского интерфейса из фонового потока, будет вызвано исключение 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, поэтому здесь можно обновлять визуальные компоненты.

Класс BackgroundTask

BackgroundTask<T, V> - это параметризованный класс:

  1. T − тип объектов, отображающих ход выполнения задачи. Объекты этого типа передаются в метод progress() задачи во время вызова TaskLifeCycle.publish() в рабочем потоке.

  2. V − тип результата задачи, передаваемый в метод done(). Результат можно также получить, вызвав метод BackgroundTaskHandler.getResult(), который будет ждать завершения задачи.

Класс BackgroundTask потокобезопасный и не определяет собственного состояния. Если вы не создаете поля для временных данных при реализации класса задачи, то вы можете запустить несколько параллельных процессов, используя один экземпляр задачи.

Методы класса BackgroundTask

Подробная информация о методах предоставлена в Javadocs для классов BackgroundTask, TaskLifeCycle и BackgroundTaskHandler.

run()

Этот метод реализует задачу. Он вызывается в отдельном рабочем потоке для выполнения задачи.

Метод должен поддерживать внешние прерывания. Для обеспечения этого периодически проверяйте флаг TaskLifeCycle.isInterrupted() во время длительных процессов и прекращайте выполнение при необходимости. Кроме того, нельзя молча отбрасывать InterruptedException (или любое другое исключение) - вместо этого необходимо либо корректно выйти из метода, либо вообще не обрабатывать исключение.

Также, с использованием метода isCancelled() можно проверить, была ли задача прервана с помощью cancel().

canceled()

Этот метод вызывается только в потоке UI во время контролируемой отмены задачи, например, когда метод cancel() вызывается в TaskHandler.

progress()

Этот метод вызывается в потоке UI, когда изменяется значение прогресса. Например, после вызова метода taskLifeCycle.publish().

done()

Этот метод вызывается в потоке UI, когда задача завершена.

handleTimeoutException()

Этот метод вызывается в потоке UI, когда истекло время ожидания задачи. Если окно, в котором выполняется задача, закрывается, задача останавливается без уведомления.

handleException()

Этот метод вызывается в потоке UI, когда возникают любые исключения.

Заметки и советы

  1. Используйте метод dialogs.createBackgroundTaskDialog() для отображения модального окна с индикатором прогресса и кнопкой Отмена. Он позволяет определить тип индикации прогресса и разрешить или запретить отмену фоновой задачи для окна.

  2. Если необходимо использовать определенные значения визуальных компонентов в потоке задачи, нужно реализовать их получение в методе getParams(), который выполняется в потоке пользовательского интерфейса после запуска задачи. В методе run() эти параметры будут доступны через метод getParams() объекта TaskLifeCycle.

  3. На выполнения фоновых задач влияют свойства приложения jmix.ui.background-task.task-killing-latency, jmix.ui.background-task.threads-count, и jmix.ui.background-task.timeout-expiration-check-interval.

Пример использования

Часто при запуске фоновой задачи необходимо отображать простой пользовательский интерфейс:

  1. Чтобы показать пользователю, что запрошенное действие выполняется.

  2. Чтобы дать пользователю возможность отменить длительную операцию.

  3. Чтобы отображать ход выполнения операции, если процент прогресса можно определить.

Это можно сделать с помощью метода createBackgroundTaskDialog() интерфейса Dialogs.

Для иллюстрации рассмотрим следующую задачу:

  1. Экран содержит таблицу данных со списком пользователей с включенной множественной выборкой.

  2. При щелчке на кнопке Отправить email система отправляет письма с напоминанием выбранным пользователям без блокировки пользовательского интерфейса и с возможностью отмены операции.

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() выполняется в фоновом потоке и не имеет доступа к компонентам пользовательского интерфейса.
4 Установка времени ожидания в 10 минут.
5 Периодическая проверка isCancelled(), чтобы немедленно остановить задачу после нажатия кнопки Cancel в диалоговом окне.
6 Отправка email. См. подробнее о отправке email здесь.
7 Обновление индикатора прогресса после каждой отправки email.