Вступление
В первой части я показал, как использовать 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 somewhereexecutor = 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 .