Статьи

Advanced ZK: асинхронные обновления пользовательского интерфейса и фоновая обработка — часть 2

Вступление

В первой части я показал, как использовать push-серверы и потоки для выполнения фоновых задач в приложении ZK. Однако у простого примера был серьезный недостаток, который делает его плохим подходом для реальных приложений: он запускает новый поток для каждой фоновой задачи.

JDK5 представил класс ExecutorService, который абстрагирует детали потоков и предоставляет нам хороший интерфейс, который можно использовать для отправки задач для фоновой обработки.

В этой записи блога я опишу наиболее важные части создания приложения ZK, которое содержит фоновое задание, которое принимает строку и возвращает ее в верхнем регистре. Полный пример проекта доступен на Github:

https://github.com/Gekkio/blog/tree/master/2012/10/async-zk-part-2

1. Создайте экземпляр ExecutorService

Сначала нам нужен ExecutorService, который мы можем использовать в нашем коде ZK. В большинстве случаев нам нужен общий одноэлементный экземпляр, который можно настроить и управлять с помощью внедрения зависимостей (например, Spring). Очень важно убедиться, что ExecutorService создается только один раз, и он корректно завершается с приложением.

В этом примере проекта я буду использовать простой класс-держатель, который управляет жизненным циклом одного статически доступного экземпляра ExecutorService. Этот держатель должен быть настроен как слушатель в zk.xml.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package sample;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.util.WebAppCleanup;
import org.zkoss.zk.ui.util.WebAppInit;
 
public class SampleExecutorHolder implements WebAppInit, WebAppCleanup {
 
    private static volatile ExecutorService executor;
 
    public static ExecutorService getExecutor() {
        return executor;
    }
 
    @Override
    public void cleanup(WebApp wapp) throws Exception {
        if (executor != null) {
            executor.shutdown();
            System.out.println('ExecutorService shut down');
        }
    }
 
    @Override
    public void init(WebApp wapp) throws Exception {
        executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        System.out.println('Initialized an ExecutorService');
    }
 
}

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

2. Напишите классы событий, которые моделируют результаты фоновой задачи

Мы будем использовать ZK-сервер для передачи результатов задачи обратно в пользовательский интерфейс, поэтому результаты должны быть смоделированы как события ZK. Лучше всегда создавать собственные подклассы Event вместо добавления результатов в параметр data, поскольку пользовательский класс более безопасен и может поддерживать несколько полей.

Первый класс событий представляет обновление состояния, которое отправляется во время выполнения задачи. В этом примере он будет содержать количество символов во входной строке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package sample;
 
import org.zkoss.zk.ui.event.Event;
 
public class FirstStepEvent extends Event {
 
    public final int amountOfCharacters;
 
    public FirstStepEvent(int amountOfCharacters) {
        super('onFirstStepCompleted', null);
        this.amountOfCharacters = amountOfCharacters;
    }
 
}

Второй класс событий представляет собой полностью выполненную задачу. В этом примере он содержит строку ввода в верхнем регистре.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package sample;
 
import org.zkoss.zk.ui.event.Event;
 
public class SecondStepEvent extends Event {
 
    public final String upperCaseResult;
 
    public SecondStepEvent(String upperCaseResult) {
        super('onSecondStepCompleted', null);
        this.upperCaseResult = upperCaseResult;
    }
 
}

3. Напишите класс задачи

Класс задачи должен иметь следующие характеристики:

  • Он реализует Runnable
  • Он принимает все необходимые входные данные в качестве аргументов конструктора (данные должны быть неизменными, если это возможно!). Эти входные данные должны быть поточно-ориентированными и, как правило, не должны содержать никаких материалов, связанных с ZK (без компонентов, сессий и т. Д.). Например, если вы хотите использовать значение Textbox в качестве ввода, прочитайте его заранее и не передавайте само Textbox в качестве аргумента .
  • В качестве аргументов конструктора требуется рабочий стол и хотя бы один EventListener. Они необходимы для отправки результатов обратно в интерфейс

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package sample;
 
import java.util.Locale;
 
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.DesktopUnavailableException;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
 
public class SampleTask implements Runnable {
 
    private final String input;
    private final Desktop desktop;
    private final EventListener<Event> eventListener;
 
    @SuppressWarnings({ 'rawtypes', 'unchecked' })
    public SampleTask(String input, Desktop desktop, EventListener eventListener) {
        this.input = input;
        this.desktop = desktop;
        this.eventListener = eventListener;
    }
 
    @Override
    public void run() {
        try {
            // Step 1
            Thread.sleep(10000);
            Executions.schedule(desktop, eventListener, new FirstStepEvent(input.length()));
 
            // Step 2
            Thread.sleep(10000);
            Executions.schedule(desktop, eventListener, new SecondStepEvent(input.toUpperCase(Locale.ENGLISH)));
        } catch (DesktopUnavailableException e) {
            System.err.println('Desktop is no longer available: ' + desktop);
        } catch (InterruptedException e) {
        }
    }
 
}

Обратите внимание, как все аргументы конструктора хранятся в закрытых конечных полях и как входные данные неизменны (строки являются неизменяемыми в Java!). Задача имитирует длительную обработку с помощью Thread.sleep и отправляет событие состояния, когда «обработка» заканчивается наполовину.

4. Расписание задач в ZK композиторов

Использовать задачу в композиторах очень просто. Вам нужно только включить push-запрос сервера и отправить новый экземпляр задачи исполнителю. Это автоматически запускает задачу, как только свободная фоновая нить становится доступной.

1
2
3
4
desktop.enableServerPush(true);
// Get the executor from somewhere
executor = SampleExecutorHolder.getExecutor();
executor.execute(new SampleTask(input.getValue(), desktop, this));

В этом примере компоновщик расширяет GenericForwardComposer, который реализует EventListener, так что он может сам обрабатывать результирующие события задачи. Оба события обрабатываются методами, которые обновляют пользовательский интерфейс информацией о состоянии.

1
2
3
4
5
6
7
public void onFirstStepCompleted(FirstStepEvent event) {
    status.setValue('Task running: ' + event.amountOfCharacters + ' characters in input');
}
 
public void onSecondStepCompleted(SecondStepEvent event) {
    status.setValue('Task finished: ' + event.upperCaseResult);
}

Заключительные слова

Используя эту технику, довольно просто добавить надежную поддержку для длительных задач в приложении ZK. Результирующий код в композиторах ZK очень прост, потому что результаты передаются с использованием типичной парадигмы Event / EventListener, которая очень распространена в приложениях ZK.

Самая большая опасность в этой технике — это ошибки безопасности потоков, которые очень сложно отладить. Крайне важно полностью понимать потоки, в которых выполняется каждый фрагмент кода, и гарантировать, что все разделяемое состояние полностью поточно-ориентировано. Использование неизменяемых входных данных и неизменяемых выходных событий обычно достаточно для обеспечения безопасности, если сама фоновая задача не имеет доступа к другим не поточно-ориентированным ресурсам. Некоторые распространенные ошибки:

  • Вызов методов локальной зависимой от потока библиотеки в фоновой задаче (например, любой метод, который кажется волшебным образом получает «текущее» значение некоторого типа). Фоновые потоки не будут автоматически содержать те же локальные значения потоков, что и потоки сервлета, поэтому по умолчанию все эти методы не будут работать. Например, Sessions.getCurrent (), Executions.getCurrent () в ZK, многие статические методы Spring Security.
  • Передача не потокобезопасных параметров в фоновую задачу. Например, передача изменяемого списка, который может быть изменен композитором во время выполнения задачи (всегда делайте копии изменяемых коллекций!).
  • Передача не-потокобезопасных данных результатов в событиях. Например, передача списка в результате события, в то время как список будет изменен позже в задаче (всегда делайте копии изменяемых коллекций!).
  • Доступ к не поточно-ориентированным методам на рабочем столе. Даже если у вас есть доступ к рабочему столу в фоновом режиме, большинство методов рабочего стола не являются поточно-ориентированными. Например, при вызове desktop.isAlive () не гарантируется правильное возвращение статуса (по крайней мере, в ZK 6.5 метод основан на энергонезависимых полях, поэтому запись не гарантируется, чтобы быть видимой в фоновом потоке)

Ссылка: Advanced ZK: Асинхронные обновления пользовательского интерфейса и фоновая обработка — часть 2 от нашего партнера по JCG Joonas Javanainen в техническом блоге Jawsy Solutions .