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

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

Основы

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

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

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

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

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

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

Метод 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() выполняется в потоке пользовательского интерфейса, поэтому здесь мы можем обновить визуальные компоненты.

Класс BackgroundTask

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

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

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

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

Методы BackgroundTask

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

run()

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

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

Метод isCancelled() возвращает true, если задача была прервана вызовом метода cancel().

canceled()

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

progress()

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

done()

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

handleTimeoutException()

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

handleException()

Этот метод вызывается в потоке UI при возникновении каких-либо исключений.

Примечания и советы

  1. Для показа пользователю модального окна с прогрессом и кнопкой Cancel используйте метод dialogs.createBackgroundWorkDialog(). Для окна можно задать режим отображения прогресса и разрешить или запретить отмену фоновой задачи.

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

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

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

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

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

  2. Дать пользователю возможность прервать запрошенное долгое действие.

  3. Показать процент выполнения, если его можно определить.

Вы можете сделать это с помощью метода createBackgroundWorkDialog() интерфейса Dialogs.

В качестве примера рассмотрим следующую задачу по разработке:

  1. Некоторый экран содержит таблицу, отображающую список клиентов, с включенным множественным выделением.

  2. По нажатию кнопки система должна послать письма-напоминания выбранным клиентам, без блокировки 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 Отправить письмо. Подробнее об отправке email читайте здесь.
7 Обновить индикатор прогресса после каждого посланного письма.