GWT(請參閱 參考資料)采用了一種不尋常的方式進行 Web 應用程序開發。它沒有采用客戶端和服務器端代碼庫的普通隔離,而是提供了一個 Java API,該 API 允許創建基於組件的 GUI,然後編譯它們,從而在用戶的 Web 浏覽器上顯示它們。 與一般的 Web 應用程序開發體驗相比,使用 GWT 更接近於使用 Swing 或 SWT 進行開發,它還試圖將 HTTP 協議和 Html DOM 模型抽象出去。實際上,應用程序最終幾乎總是會呈現在 Web 浏覽器中。
GWT 是通過代碼生成來實現這些功能的,它利用其編譯器從客戶端 Java 代碼生成 JavaScript。GWT 支持 Java.lang
和 Java.util
包的子集,還支持 GWT 自身提供的 API。編譯後的 GWT 應用程序由 Html、XML 和 Javascript 片段組成。但是,這些片段很難區分,所以最好把編譯後的應用程序當成是黑盒子 —— Java 字節碼的 GWT 等價物。
在這篇文章中,我將創建一個簡單的 GWT 應用程序,用該程序從遠程 Web API 獲得天氣報告,並在浏覽器中顯示它。在整個過程中,我將簡要介紹盡可能多的 GWT 功能,還將提到一些可能遇到的潛在問題。
從簡單的開始
清單 1 顯示了可以用 GWT 制作的最簡單的應用程序的 Java 源代碼:
public class Simple implements EntryPoint { public void onModuleLoad() { final Button button = new Button("Say 'Hello'"); button.addClickListener(new ClickListener() { public void onClick(Widget sender) { Window.alert("Hello World!"); } }); RootPanel.get().add(button); }}
這個代碼看起來非常像使用 Swing、AWT 或 SWT 編寫的 GUI 代碼。不出所料,清單 1 創建了一個按鈕,在單擊此按鈕時會顯示消息 “Hello World!”。該按鈕被添加到 RootPanel
,這是一個環繞 Html 頁面主體的 GWT 包裝對象。圖 1 顯示了應用程序在 GWT Shell 中運行時的情況。GWT Shell 是一個包含在 GWT SDK 中的調試宿主環境(debugging hosting environment),與一個簡單的浏覽器組合在一起。
構建 Weather Reporter 應用程序
我將用 GWT 創建一個簡單的 Weather Reporter 應用程序。該應用程序的 GUI 向用戶顯示了一個用於輸入 ZIP 代碼的輸入框,還顯示了一個使用攝氏溫度還是華氏溫度來表示溫度的選項。當用戶單擊 Submit 按鈕時,該應用程序用 Yahoo! 的免費天氣 API 獲得所選定地區的 RSS 格式的報告。然後提取這個文檔的 Html 部分,並將它顯示給用戶。
GWT 應用程序被打包成模塊,並且必須符合特定的結構。名為 module-name.gwt.XML 的配置文件定義了充當應用程序入口點的類,並指明是否要從其他 GWT 模塊繼承資源。在應用程序的源包結構中,必須將配置文件放在與 clIEnt
包和 public 目錄相同的級別上,所有客戶端 Java 代碼都在 clIEnt 包中,而 public 目錄包含項目的 Web 資源,比如圖片、CSS 和 HTML。最後,public 目錄中必須包含一個 Html 文件,該文件中必須有一個包含模塊的限定名稱的 meta
標記。GWT 的運行時 JavaScript 庫使用這個文件來初始化應用程序。
在指定了入口點類的情況下,GWT 的 applicationCreator
會替您生成這個基本結構。所以可以將調用 applicationCreator developerworks.gwt.weather.clIEnt.Weather
生成一個項目框架作為創建 Weather Reporter 應用程序的起點。在該應用程序的源代碼下載中包含的 Ant 構建文件中,有一些有用的目標(target),可使用它們讓 GWT 項目符合這個結構。(請參閱 下載)。
開發基本的 GUI
首先,我將開發應用程序的用戶界面小部件(widget)的基本布局,且不添加其他任何行為。Widget
類是可以呈現在 GWT UI 中的幾乎所有類的超類。Widget
總是包含在 Panel
中,Panel 本身也是 Widget
,所以可以被嵌套。不同類型的面板提供了不同的布局行為。所以,GWT Panel
扮演的角色與 AWT/Swing 中的 Layout
或 XUL 中的 Box
類似。
所有小部件和面板最終都要附加到包含它們的 Web 頁面上。如 清單 1 所示,可以直接把它們附加到 RootPanel
上。或者,可以用 RootPanel
獲得對使用 ID 或類名標識的 HTML 元素的引用。在這個示例中,我將使用兩個獨立的 Html DIV
元素,它們的名稱分別是 input-container
和 output-container
。第一個元素包含 Weather Reporter 應用程序的 UI 控件,第二個元素顯示天氣報告本身。
清單 2 顯示了設置基本布局所需的代碼;它應當是自解釋的。Html
小部件只是 HTML 標記的容器,來自 Yahoo! 天氣種子(weather feed)的 Html 輸出將顯示在這裡。這些代碼都位於 Weather
類的 onModuleLoad()
方法中,這個方法由 EntryPoint
接口提供。在將包含天氣模塊的 Web 頁面裝入客戶機的 Web 浏覽器時,將調用這個方法。
public void onModuleLoad() { HorizontalPanel inputPanel = new HorizontalPanel(); // Align child widgets along middle of panel inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); Label lbl = new Label("5-digit zipcode: "); inputPanel.add(lbl); TextBox txBox = new TextBox(); txBox.setVisibleLength(20); inputPanel.add(txBox); // Create radio button group to select units in C or F Panel radioPanel = new VerticalPanel(); RadioButton ucRadio = new RadioButton("units", "Celsius"); RadioButton ufRadio = new RadioButton("units", "Fahrenheit"); // Default to Celsius ucRadio.setChecked(true); radioPanel.add(ucRadio); radioPanel.add(ufRadio); // Add radio buttons panel to inputs inputPanel.add(radioPanel); // Create Submit button Button btn = new Button("Submit"); // Add button to inputs, aligned to bottom inputPanel.add(btn); inputPanel.setCellVerticalAlignment(btn, HasVerticalAlignment.ALIGN_BOTTOM); RootPanel.get("input-container").add(inputPanel); // Create widget for HTML output HTML weatherHtml = new HTML(); RootPanel.get("output-container").add(weatherHtml);}
圖 2 顯示了在 GWT Shell 中呈現的布局:
用 CSS 添加樣式
呈現的 Web 頁面看起來很傻,所以它將從 CSS 樣式規則中汲取一些優點。可以用兩種方式為 GWT 應用程序添加樣式。首先,默認情況下,每個小部件都有一個 CSS 類名,其形式為 project-widget
。例如,gwt-Button
和 gwt-RadioButton
是兩個核心 GWT 小部件類名。面板通常被實現為一堆嵌套式表格,所以沒有默認的類名。
每個小部件類型一個類名(classname-per-widget-type)的默認方法使得在整個應用程序中一致地設置小部件樣式變得非常容易。當然,普通的 CSS 選擇器規則也可以應用,所以可以根據小部件的上下文,用選擇器規則在同一小部件上應用不同的樣式。要得到更多的靈活性,則可以調用小部件的 setStyleName()
和 addStyleName()
方法,臨時替換和增加小部件的默認類名。
清單 3 組合了這些方法,把樣式應用到 Weather Reporter 應用程序的輸入面板上。通過對 inputPanel.setStyleName("weather-input-panel");
的調用,在 Weather.Java 中創建了 weather-input-panel
類名。
/* Style the input panel itself */.weather-input-panel { background-color: #AACCFF; border: 2px solid #3366CC; font-weight: bold;}/* Apply padding to every element within the input panel */.weather-input-panel * { padding: 3px;}/* Override the default button style */.gwt-Button { background-color: #3366CC; color: white; font-weight: bold; border: 1px solid #AACCFF;}/* Apply a hover effect to the button */.gwt-Button:hover { background-color: #FF0084;}
圖 3 顯示了應用程序被替換成這些樣式之後的情況:
添加客戶端行為
現在應用程序的基本布局和樣式已經就緒,我將開始實現一些客戶端行為。可以用熟悉的偵聽器模式在 GWT 中執行事件處理。GWT 為鼠標事件、鍵盤事件、修改事件等提供了 Listener
接口,還提供了幾個適配器和助手類,以獲得更多方便。
一般情況下使用 Swing 程序員熟悉的內部類形式來添加事件偵聽器。但是,所有 GWT Listener
方法的第一個參數都是事件的發送者,通常是用戶剛剛與之交互的小部件。這意味著可以把同一個 Listener
實例附加到所需的多個小部件上;可以用 sender 參數確定是哪個小部件觸發了事件。
清單 4 顯示了 Weather Reporter 應用程序中實現的兩個事件偵聽器。click 句柄被添加到了 Submit 按鈕上,keyhandler 被添加到了 TextBox
上。不管是單擊 Submit 按鈕,還是在 TextBox
擁有焦點時按下回車鍵,都會導致相關的句柄調用私有的 validateAndSubmit()
方法。在添加到清單 4 的代碼中之後,txBox
和 ucRadio
已經成為 Weather
類的實例變量,所以可以從驗證方法訪問它們。
// Create Submit button, with click listener inner class attachedButton btn = new Button("Submit", new ClickListener() { public void onClick(Widget sender) { validateAndSubmit(); }});// For usability, also submit data when the user hits Enter // when the textbox has focustxBox.addKeyboardListener(new KeyboardListenerAdapter(){ public void onKeyPress(Widget sender, char keyCode, int modifiers) { // Check for Enter key if ((keyCode == 13) && (modifIErs == 0)) { validateAndSubmit(); } } });
清單 5 顯示了 validateAndSubmit()
方法的實現。該實現非常簡單,由封裝驗證邏輯的 ZipCodeValidator
類完成。如果用戶沒有輸入正確的 5 位數字的 ZIP 代碼,那麼 validateAndSubmit()
將在警告框中顯示錯誤消息,如果這種情況出現在 GWT 中,則會調用 Window.alert()
。如果 ZIP 代碼正確,那麼它將與用戶對攝氏或華氏溫度單位的選擇一起被傳遞給 fetchWeatherHtml()
方法,這個方法稍後再介紹。
private void validateAndSubmit() { // Trim whitespace from input String zip = txBox.getText().trim(); if (!zipValidator.isValid(zip)) { Window.alert("Zip-code must have 5 digits"); return; } // Disable the TextBox txBox.setEnabled(false); // Get choice of celsius/fahrenheit boolean celsius = ucRadio.isChecked(); fetchWeatherHtml(zip, celsius);}
用 GWT Shell 進行客戶端調試
在這裡我要岔開一會,提一下 GWT Shell,它擁有允許在 Java IDE 中調試客戶端代碼的 JVM 掛鉤。您可以與 Web UI 進行交互,分步調試表示客戶端執行的相應 Javascript 代碼的 Java 代碼。這是一項很重要的功能,因為在客戶端上調試所生成的 JavaScript 基本上是不可能的。
可以很容易地配置一個 Eclipse 調試任務,從而通過 com.google.gwt.dev.GWTShell
類啟動 GWT Shell。圖 4 顯示了按下 Submit 按鈕後,在 validateAndSubmit()
方法的斷點處暫停的 Eclipse:
與服務器端組件進行通信
現在 Weather Reporter 應用程序就可以搜集和驗證用戶輸入了。下一步是從服務器中檢索數據。在正常的 AJax 開發中,需要直接從 Javascript 調用服務器端資源,並接收編碼成 JavaScript Object Notation(JSON)或 XML 的數據。GWT 在自己的遠程過程調用(remote procedure call,RPC)機制背後抽象這個通信過程。
在 GWT 的術語中,客戶機代碼與運行在 Web 服務器上的服務 進行通信。用來公開這些服務的 RPC 機制與 Java RMI 使用的方法類似。這意味著只需要編寫服務的服務器端實現和兩個接口即可。代碼生成和反射將負責處理客戶機存根和服務器端主干代理(server-side skeleton proxIEs)。
相應地,要做的第一步是定義 Weather Reporter 服務的接口。這個接口必須擴展 GWT RemoteService
接口,它包含應該公開給 GWT 客戶機代碼的服務方法的簽名。因為 GWT 中的 RPC 調用是在 Javascript 代碼和 Java 代碼之間進行的,所以 GWT 集成了對象序列化機制,用它來協調跨語言分界(language divide)的參數和返回值(請參閱 可序列化類型 側欄,了解您可以使用哪些可序列化類型)。
GWT 下可序列化類型的簡要概括如下:
int
)和基本包裝對象類(例如 Integer
)是可序列化的。
String
和 Date
是可序列化的。
IsSerializable
接口,那麼自定義類是可序列化的。
Collection
類可以與 Javadoc 注釋結合使用,通過注釋聲明它們包含的可序列化類型。 因為客戶機代碼被限制在 GWT 實現的 Java 類的一個很小的子集上,所以這些可序列化類型的覆蓋面相當廣泛。
定義了服務接口之後,下一步就是在擴展 GWT 的 RemoteServiceServlet
類的類中實現該接口。顧名思義,這是 Java 語言的 HttpServlet
的一個具體類,所以可以將它放在任何 servlet 容器中。
這裡值得一提的一個 GWT 特性是:服務的遠程接口必須位於應用程序的 clIEnt
包中,因為需要將它集成到 Javascript 的生成過程中。但是,因為服務器端實現類引用了遠程接口,所以現在在服務器端和客戶機代碼之間存在一個 Java 編譯時依賴項。對於這個問題,我的解決方案是將遠程接口放在 clIEnt
的 common
子包中。然後在 Java 構建中包含 common
包,但不包含 clIEnt
包中的剩余部分。這可以確保客戶機代碼生成的類文件只是那些需要轉換成 JavaScript 的文件。更好的解決方案是將包結構分解成兩個源目錄,一個負責客戶端代碼,一個負責服務器端代碼,然後將公共類復制到兩個目錄中。
清單 6 顯示了 Weather Reporter 應用程序使用的遠程服務接口 WeatherService
。它接受 ZIP 代碼和攝氏/華氏標記作為輸入,返回包含 Html 天氣描述的 String
。清單 6 顯示了 YahooWeatherServiceImpl
的框架,它使用 Yahoo! 的天氣 API 獲得給定 ZIP 代碼的 RSS 天氣種子,並從中獲得 Html 描述。
public interface WeatherService extends RemoteService { /** * Return HTML description of weather * @param zip zipcode to fetch weather for * @param isCelsius true to fetch temperatures in celsius, * false for fahrenheit * @return HTML description of weather for zipcode area */ public String getWeatherHtml(String zip, boolean isCelsius) throws WeatherException;} public class YahooWeatherServiceImpl extends RemoteServiceServlet implements WeatherService { /** * Return HTML description of weather * @param zip zipcode to fetch weather for * @param isCelsius true to fetch temperatures in celsius, * false for fahrenheit * @return HTML description of weather for zipcode area */ public String getWeatherHtml(String zip, boolean isCelsius) throws WeatherException { // Clever programming goes here }}
從這時起,就開始脫離標准的 RMI 方法。因為來自 JavaScript 的 AJax 調用是異步的,所以需要做些額外的工作來定義客戶機代碼用來調用服務的異步接口。異步接口的方法簽名與遠程接口的方法簽名有所不同,所以 GWT 要依靠 Magical Coincidental Naming。換句話說,在異步接口和遠程接口之間不存在靜態的編譯時關系,但是 GWT 會通過命名約定來指出該關系。清單 7 顯示了 WeatherService
的異步接口:
public interface WeatherServiceAsync { /** * Fetch HTML description of weather, pass to callback * @param zip zipcode to fetch weather for * @param isCelsius true to fetch temperatures in celsius, * false for fahrenheit * @param callback Weather HTML will be passed to this callback handler */ public void getWeatherHtml(String zip, boolean isCelsius, AsyncCallback callback);}
可以看到,一般的想法是創建叫做 MyServiceAsync
的接口,並提供與每個方法簽名對等的事物,然後刪除所返回類型,添加類型為 AsyncCallback
的額外參數。異步接口必須放在與遠程接口相同的包中。AsyncCallback
類有兩個方法:onSuccess()
和 onFailure()
。如果對服務的調用成功,則用服務調用的返回值調用 onSuccess()
。如果遠程調用失敗,則調用 onFailure()
,並傳遞由該服務生成的 Throwable
,以表示失敗的原因。
從客戶機調用服務
有了 WeatherService
和它的異步接口之後,現在就可以修改 Weather Reporter 客戶機,從而調用服務並處理服務器響應。第一步只是公式化地設置代碼:通過調用 GWT.create(WeatherService.class)
並向下傳送所返回的對象,創建一個在 Weather 客戶機上使用的 WeatherServiceAsync
實例。接下來,必須將 WeatherServiceAsync
強行轉換成 ServiceDefTarget
,這樣才能在它上面調用 setServiceEntryPoint()
。setServiceEntryPoint()
指向對應的遠程服務實現所部署的 URL 上的 WeatherServiceAsync
存根。請注意,這實際上是在編譯時硬編碼的。因為這個代碼成為在 Web 浏覽器中部署的 JavaScript,所以沒辦法在運行時從屬性文件中查找這個 URL。顯然,這限制了編譯後的 GWT Web 應用程序的移植性。
清單 8 顯示了 WeatherServiceAsync
對象的設置,然後給出了 fetchWeatherHtm()
的實現,這個實現我在前面提到過(請參閱 添加客戶端行為):
// Statically configure RPC serviceprivate static WeatherServiceAsync ws = (WeatherServiceAsync) GWT.create(WeatherService.class);static { ((ServiceDefTarget) ws).setServiceEntryPoint("ws");}/** * Asynchronously call the weather service and display results */private void fetchWeatherHtml(String zip, boolean isCelsius) { // Hide existing weather report hideHtml(); // Call remote service and define callback behavior ws.getWeatherHtml(zip, isCelsius, new AsyncCallback() { public void onSuccess(Object result) { String html = (String) result; // Show new weather report displayHtml(Html); } public void onFailure(Throwable caught) { Window.alert("Error: " + caught.getMessage()); txBox.setEnabled(true); } });}
對服務的 getWeatherHtml()
的實際調用實現起來非常簡單:使用一個匿名回調句柄類將服務器的響應傳遞給顯示響應的方法即可。
圖 5 顯示了應用程序的運行情況,顯示了從 Yahoo! 天氣 API 檢索的天氣報告:
服務器端驗證的需要
用 GWT 合並客戶端和服務器端代碼存在內在危險。因為您使用 Java 語言來編寫所有代碼,所以 GWT 的抽象隱藏了客戶機/服務器之間的分離,很容易讓人誤認為可以相信運行時的客戶端代碼。這是錯誤的。Web 浏覽器上運行的任何代碼都可能被惡意用戶篡改或者完全繞開。GWT 提供了高層次的混淆,從而可以將這個問題降低到一定程度,但是仍然存在次要攻擊點:GWT 客戶機及其服務之間的 HTTP 通信量。
假設我是一個攻擊者,想利用 Weather Reporter 應用程序的弱點。圖 6 顯示了 Microsoft 的 Fiddler 工具,它攔截了從 Weather Reporter 客戶機到運行在服務器之上的 WeatherService
的請求。攔截到請求之後,Fiddler 允許對請求的任意部分進行修改。高亮的文本顯示了我找到的指定 ZIP 代碼在請求中的編碼位置。現在我可以將 ZIP 代碼更改為任何我喜歡的值,大致范圍是從 “10001” 到 “XXXXX”。
現在,假設 YahooWeatherServiceImpl
中有一些服務器端代碼對 ZIP 代碼調用了 Integer.parseInt()
。ZIP 代碼最終一定會通過集成到 Weather
的 validateAndSubmit()
方法中的驗證檢查。正如已經看到的那樣,這個檢查已經被破壞,拋出了一個 NumberFormatException
。
在這個示例中,沒有發生什麼可怕的事情,攻擊者只是在客戶機上看到了一條錯誤消息。但是,對於處理更敏感數據的 GWT 應用程序進行全面攻擊也是有可能的。假設 ZIP 代碼被替換成了訂單跟蹤應用程序中的客戶 ID 號碼。攔截和修改這個值可能暴露其他客戶的敏感財務信息。在數據庫查詢可以使用數據值的任何地方,同樣的方式都有可能導致 SQL 注入攻擊。
對於以前曾經使用過 AJax 應用程序的人來說,這些不應是天方夜譚。只需要雙擊任何輸入值,就可以在服務器上重新驗證它們。關鍵是要記住:在 GWT 應用程序中編寫的一些 Java 代碼在運行時實際上是不可信任的。但是,確實還有一線希望可以解決這個 GWT 問題。在 Weather Reporter 應用程序中,我編寫了一個在客戶機上使用的 ZipCodeValidator
,可以將它移入 clIEnt.common
包,並在服務器端重用相同的驗證。清單 9 顯示了集成到 YahooWeatherServiceImpl
中的這個檢查程序:
public String getWeatherHtml(String zip, boolean isCelsius) throws WeatherException { if (!new ZipCodeValidator().isValid(zip)) { log.warn("Invalid zipcode: "+zip); throw new WeatherException("Zip-code must have 5 digits"); }
用 JSNI 調用本機 JavaScript
可視效果庫在 Web 應用程序開發中變得越來越流行,不論它們的效果只是用來提供細微的用戶交互線索還是僅僅用於裝飾。我想給 Weather Reporter 應用程序添加一些吸引眼球的東西。GWT 沒有提供這類功能,但是它的 Javascript 本機接口(JSNI)提供了解決方案。JSNI 允許直接在 GWT 客戶機 Java 代碼中進行 JavaScript 調用。這意味著我可以利用來自 Scriptaculous 庫的效果(請參閱 參考資料)或來自 Yahoo! 用戶界面庫的效果。
JSNI 巧妙地把 Java 語言的 native
關鍵字和嵌入特殊注釋塊中的 JavaScript 組合在一起。用示例對此進行解釋可能是最好的方法,所以清單 10 顯示了一個方法,該方法調用了 Element
上的指定 Scriptaculous 效果:
/** * Publishes HTML to the weather display pane */private void displayHtml(String html) { weatherHtml.setHTML(html); applyEffect(weatherHtml.getElement(), "Appear");}/** * ApplIEs a Scriptaculous effect to an element * @param element The element to reveal */private native void applyEffect(Element element, String effectName) /*-{ // Trigger named Scriptaculous effect $wnd.Effect[effectName](element);}-*/;
這是非常有效的 Java 代碼,因為編譯器只看到 private native void applyEffect(Element element, String effectName);
。GWT 將解析注釋塊的內容,並逐字地輸出 JavaScript。GWT 提供了 $wnd
和 $doc
變量,它們分別代表窗口和文檔對象。在這個示例中,我只是訪問頂級 Scriptaculous Effect
對象,並用 JavaScript 的方括號對象存取器語法調用調用方指定的命名函數。Element
類型是 GWT 提供的 “魔法” 類型,它在 Java 和 JavaScript 代碼中都代表 Widget
的底層 Html DOM 元素。String
是可以通過 JSNI 在 Java 代碼和 JavaScript 之間透明傳遞的少數類型之一。
現在我有了一個天氣報告,當數據從服務器返回時,該天氣報告逐漸淡化消失。最後一項操作是在效果完成時重新啟用 ZIP 代碼 TextBox
。Scriptaculous 使用異步回調機制把特殊的生命周期通知給偵聽器。在這裡,事情變得稍微有點復雜,因為我需要通過回調 JavaScript 使它回到 GWT 客戶機的 Java 代碼中。在 Javascript 中,可以用任意數量的參數調用函數,所以 Java 風格的方法重載已不存在。這意味著 JSNI 需要使用一個笨拙的語法來引用 Java 方法,以消除可能的重載歧義。GWT 文檔是這樣說明這個語法的:
[instance-expr.]@class-name::method-name(param-signature)(arguments)
instance-expr.
部分是可選的,因為靜態方法被調用時不需要對象引用。同樣,用示例來查看它的效果是最容易的,如清單 11 所示:
/** * Applies a Scriptaculous effect to an element * @param element The element to reveal */private native void applyEffect(Element element, String effectName) /*-{ // Keep reference to self for use inside closure var weather = this; // Trigger named Scriptaculous effect $wnd.Effect[effectName](element, { afterFinish : function () { // Make call back to Weather object weather.@developerworks.gwt.weather.clIEnt.Weather::effectFinished()(); } });}-*/;/** * Callback triggered when a Scriptaculous effect finishes. * Re-enables the input textbox. */private void effectFinished() { this.txBox.setEnabled(true); this.txBox.setFocus(true);}
applyEffect()
方法已經被更改為將額外的 afterFinish
參數傳遞給 Scriptaculous。afterFinish
的值是一個匿名函數,在效果完成時被調用。這與 GWT 事件句柄中使用的內部類的概念有點相似。對 Java 代碼實際進行回調時,要指定將在該代碼上激活調用的 Weather
對象,然後指定 Weather
類的完整規范名稱,這之後是指定將要調用的函數的名稱。第一對空的括號指明將調用不帶參數的 effectFinished()
方法。第二對括號調用函數。
這裡的秘訣在於:本地變量 weather
保存了 this
引用的一個副本。根據 JavaScript 調用語義操作的方式,afterFinish
函數中的 this
變量實際上是一個 Scriptaculous 對象,因為將由 Scriptaculous 進行這個函數調用。請在封裝之外做一份 this
的副本,這是一項簡單的工作。
現在已經演示了 JSNI 的一些功能,還應當指出的是,把 Scriptaculous 集成到 GWT 的更佳方式是將 Scriptaculous 效果功能包裝成定制的 GWT 小部件。這正是 Alexei Sokolov 在 GWT 組件庫中所做的工作(請參閱 參考資料)。
現在就完全完成了 Weather Reporter 應用程序,我將回顧一下用 GWT 進行 Web 開發的優缺點。
為什麼使用 GWT?
與您的預期可能有所不同,GWT 應用程序顯然不太類似於 Web 應用程序。GWT 實際上把浏覽器作為輕量級 GUI 應用程序的運行時環境,結果,使用 GWT 進行開發更接近於使用 Morfik、OpenLaszlo 甚至 Flash 進行開發,而不太像是一般的 Web 應用程序開發。所以,GWT 最適合的 Web 應用程序是能夠作為單一頁面的豐富 Ajax GUI 存在的應用程序。Google 最近的一些 beta 發行版(如日歷和電子表應用程序)都具有這樣的特性,這可能不是什麼巧合。它們是一些很棒的應用程序,但是不能用這種方式解決所有的業務場景。大多數 Web 應用程序非常適合以頁面為中心的模型,而 AJax 允許在需要的地方使用更豐富的交互范例。GWT 不太適合傳統的以頁面為中心的應用程序。雖然可以把 GWT 小部件與普通的 Html 表單輸入組合,但 GWT 小部件的狀態與頁面的其他部分是分開的。例如,沒有某種簡單的方法可以把 GWT Tree
小部件中選定的值作為普通表單的一部分一起提交。
GWT 的運行庫在 apache License 2.0 下授權,可以免費使用 GWT 創建商業應用程序。但是,GWT 工具鏈只以二進制形式提供,且不允許修改。該工具鏈中包括 Java-to-Javascript 編譯器。這意味著生成的 JavaScript 中的任何錯誤都超出了您的控制。一個特殊問題是 GWT 對用戶代理檢測的依賴:新發行的每個浏覽器都需要對 GWT 工具箱進行更新,以提供支持。
如果決定把 GWT 用於 J2EE 應用程序環境,那麼 GWT 的設計會使集成變得相對簡單。在這個場景中,GWT 服務應該被當成與 Struts 中的 Action
類似的東西 —— 一個很薄的中間層,它只代理對後端業務邏輯調用的 Web 請求。因為 GWT 服務就是 HTTP servlet,所以可以容易地將它集成到 Struts 或 SpringMVC 中,例如放在身份驗證過濾器後面。
GWT 確實有一些非常顯眼的缺陷。首先,它缺乏對功能退化的預防。現代 Web 應用程序開發中的最佳實踐是創建沒有 JavaScript 的頁面,然後在可以使用 JavaScript 的地方用它修飾和添加額外的行為。在 GWT 中,如果 Javascript 不可用,則根本得不到 UI。對於某些 Web 應用程序類型來說,這簡直是不可接受的。國際化也是 GWT 的一個主要問題。因為 GWT 客戶機 Java 類在浏覽器中運行,所以不能通過在運行時訪問屬性或資源綁定來得到本地化的字符串。現在有一個復雜的工作區,它需要為每個地區創建的客戶端類的子類(請參閱 參考資料),但是 GWT 的工程師正在開發更可行的解決方案。
在代碼生成的情形中
GWT 架構中最具爭議的問題可能就是在客戶端代碼中對 Java 語言的切換。有些 GWT 的擁護者認為用 Java 語言編寫客戶端代碼實際上要比編寫 JavaScript 好。並不是所有人都贊成這個觀點,許多 JavaScript 程序員極不情願犧牲他們語言的靈活性和表現力,來獲得有時非常繁重的 Java 開發工作。用 Java 代碼代替 Javascript 比較有吸引力的一種情況就是:團隊缺少有經驗的 Web 開發人員。但是,如果團隊正在轉向 AJax 開發,那麼最好是雇傭有經驗的 JavaScript 程序員,而不要依靠 Java 程序員利用私有的工具生成混亂的 JavaScript。由於 GWT 擴展到 JavaScript、HTTP 和 HTML 的漏洞所導致的 bug 是不可避免的,所以缺乏經驗的 Web 程序員要花很長時間跟蹤它們。作為開發人員和博客,Dimitri Glazkov 指出:“如果不能處理 Javascript,就不應當編寫 Web 應用程序的代碼。Html、CSS 和 JavaScript 是這條路上的三個必備條件。”(請參閱 參考資料)。
有些人認為因為有了靜態類型化和編譯時檢測,Java 編碼天生就比 Javascript 編程不容易出錯。這是一個相當靠不住的論調。用任何語言都可能編寫糟糕的代碼,大量充滿 bug 的 Java 應用程序就是證明。也可以依靠 GWT 的代碼生成來消除 bug。但是,離線語法檢測和客戶端代碼的驗證無疑會帶來一些好處。JavaScript 也可以用 Douglas Crockford 的 JSLint 的形式得到它(請參閱 參考資料)。GWT 在單元測試上有優勢,可以為客戶端代碼提供 JUnit 集成。單元測試支持仍然是 JavaScript 很欠缺的一個領域。
在開發 Weather Reporter 應用程序時,我發現客戶端 Java 代碼最引人注目的情況是它在兩個層上共享一些驗證類的能力。這顯然減少了開發勞動。跨 RPC 傳遞的任何類都適用這種情況;只需要編碼一次,就可以將它們用在客戶機和服務器代碼中。不幸的是,抽象是有漏洞的:例如,在我的 ZIP 代碼驗證器中,本想使用正則表達式執行檢測。但是,GWT 沒有實現 String.match()
方法。而且即使它實現了這個方法,在將 GWT 中的正則表達式部署到客戶機和服務器代碼時,也存在語義上的差異。這是因為 GWT 對宿主環境底層的正則表達式機制的依賴也是不完美抽象所帶來問題的一個例子。
GWT 非常被看好的一個重要原因是它的 RPC 機制和內置在 Java 代碼和 Javascript 之間的對象序列化。這消除了普通 AJax 應用程序中可以看到的許多繁重工作。但是,它是有前提的。如果想使用這個功能而不使用 GWT 的其他部分,那麼 Direct Web Remoting(DWR,它用 Java 代碼和 JavaScript 之間的對象偽裝提供了 RPC 功能)非常值得考慮(請參閱 參考資料)。
對於將 AJax 應用程序開發的一些低層方面(如跨浏覽器的不兼容、DOM 事件模型和進行 AJax 調用)抽象出來,GWT 做得很好。但是現代的 JavaScript 工具包(例如 Yahoo! UI 庫、Dojo 和 MochiKit)都提供了類似級別的抽象,卻不需要求助於代碼生成。此外,所有這些工具包都是開源的,所以可以對其進行定制,以滿足自己的需求,或者在出現 bug 的時候進行修補。對於黑盒子式的 GWT,這是不可能的。(請參閱 許可 側欄)。
結束語
GWT 是一個全面的框架,提供了許多有用的功能。但是,GWT 並不是萬能的,它針對的只是 Web 應用程序開發市場中一個相對狹窄的市場。我希望這份簡要的介紹能讓您對 GWT 的功能和局限性有一定的了解。雖然 GWT 肯定不會滿足每個人的需求,但它仍然是一個主要的技術成果,在設計下一個 AJax 應用程序時,值得認真地考慮 GWT。與我在這裡介紹的相比,GWT 具有更廣的廣度和更深的深度,所以請閱讀 Google 的文檔,以了解更多內容,或者加入 GWT 開發人員論壇上的討論