介紹
在第1部分中,我展示了如何在ZK應用程序中使用服務器推送和線程來執行后臺任務。 但是,這個簡單的示例具有一個重大缺陷,這使其對于實際應用程序而言是一種不好的方法:它為每個后臺任務啟動了一個新線程。
JDK5引入了ExecutorService類,該類抽象了線程詳細信息,并為我們提供了一個不錯的接口,可用于提交任務以進行后臺處理。
在這篇博客文章中,我將描述創建ZK應用程序的最重要部分,該應用程序包含一個采用字符串并以大寫形式返回的后臺任務。 完整的示例項目可在Github上找到:
https://github.com/Gekkio/blog/tree/master/2012/10/async-zk-part-2
1.創建一個ExecutorService實例
首先,我們需要一個可以在ZK代碼中使用的ExecutorService。 在大多數情況下,我們需要一個共享的單例實例,該實例可以通過依賴項注入(例如Spring)進行配置和管理。 確保只創建一次ExecutorService,并且使用應用程序將其正確關閉是非常重要的。
在這個示例項目中,我將使用一個簡單的holder類,該類管理單個靜態可用的ExecutorService實例的生命周期。 該持有人必須在zk.xml中配置為偵聽器 。
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;}@Overridepublic void cleanup(WebApp wapp) throws Exception {if (executor != null) {executor.shutdown();System.out.println('ExecutorService shut down');}}@Overridepublic void init(WebApp wapp) throws Exception {executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());System.out.println('Initialized an ExecutorService');}}
請注意,線程池是根據系統中的處理器使用固定大小配置的。 正確的線程池大小調整非常重要,并且取決于您打算執行的任務類型。 最大線程數也是同時進行的并發任務的最大數量!
2.編寫對后臺任務的結果進行建模的事件類
我們將使用ZK服務器推送將任務結果傳達回UI,因此必須將結果建模為ZK事件。 創建自定義Event的子類而不是將結果添加到data參數中始終是一個好主意,因為自定義類更加類型安全并且可以支持多個字段。
第一個事件類表示任務仍在運行時發送的狀態更新。 在此示例中,它將包含輸入字符串中的字符數。
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;}}
第二個事件類表示完全完成的任務。 在此示例中,它包含大寫的輸入字符串。
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相關的內容(無組件,會話等)。 例如,如果要使用文本框值作為輸入,請事先讀取該值,并且不要將文本框本身作為參數傳遞 。
- 它需要一個Desktop,以及至少一個EventListener作為構造函數參數。 它們是將結果發送回UI所必需的
在此示例中,唯一的輸入數據是將用于計算任務結果的字符串。
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;}@Overridepublic void run() {try {// Step 1Thread.sleep(10000);Executions.schedule(desktop, eventListener, new FirstStepEvent(input.length()));// Step 2Thread.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) {}}}
注意所有構造函數參數如何存儲在私有的final字段中,以及輸入數據如何不可變(Java中字符串是不可變的!)。 該任務通過使用Thread.sleep模擬長時間運行的處理,并在“處理”完成一半時提交狀態事件。
4.在ZK作曲家中安排任務
在作曲家中使用任務非常簡單。 您只需要啟用服務器推送,并將新的任務實例提交給執行者。 一旦有可用的后臺線程可用,它將自動啟動任務。
desktop.enableServerPush(true);
// Get the executor from somewhere
executor = SampleExecutorHolder.getExecutor();
executor.execute(new SampleTask(input.getValue(), desktop, this));
在此示例中,編輯器擴展了GenericForwardComposer,該實現實現了EventListener,因此它本身可以處理產生的任務事件。 這兩個事件均由使用狀態信息更新UI的方法處理。
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應用程序中非常常見。
這種技術的最大危險是線程安全錯誤,這些錯誤很難調試。 完全了解執行每段代碼的線程,并確保所有共享狀態都是完全線程安全的,這至關重要。 只要后臺任務本身不訪問其他非線程安全資源,使用不可變的輸入數據和不可變的輸出事件通常足以確保安全。 一些常見的錯誤是:
- 在后臺任務中調用線程本地相關的庫方法(例如,任何看起來神奇地獲得某種“當前”值的方法)。 后臺線程不會自動包含與servlet線程相同的線程本地值,因此默認情況下,所有這些方法都將失敗。 例如ZK中的Sessions.getCurrent(),Executions.getCurrent()和許多Spring Security靜態方法。
- 將非線程安全參數傳遞給后臺任務。 例如,傳遞一個可變的List,該可變的List可能在任務運行時由編寫者修改(總是制作可變集合的副本!)。
- 在事件中傳遞非線程安全的結果數據。 例如,在結果事件中傳遞列表,而稍后將在任務中修改列表(始終復制可變集合!)。
- 在桌面中訪問非線程安全的方法。 即使您可以在后臺任務中訪問桌面,大多數桌面方法也不是線程安全的。 例如,不能保證調用desktop.isAlive()能夠正確返回狀態(至少在ZK 6.5中,該方法依賴于非易失性字段,因此不能保證在后臺線程中可見寫入)
參考: Advanced ZK:異步UI更新和后臺處理– Jawsy Solutions技術博客博客上的JCG合作伙伴 Joonas Javanainen的第二部分 。
翻譯自: https://www.javacodegeeks.com/2012/10/advanced-zk-asynchronous-ui-updates-and-background-processing-part-2.html