級(jí)別: 高級(jí)
Philip McCarthy (philmccarthy@gmail.com), 軟件開(kāi)發(fā)顧問(wèn), Independent
2006 年 7 月 17 日
最近發(fā)布的 Google Web Toolkit (GWT) 是一組全面的 API 和工具,它支持用戶幾乎完全使用 Java™ 代碼來(lái)創(chuàng)建動(dòng)態(tài) Web 應(yīng)用程序。Philip McCarthy 回到了他廣受歡迎的面向 Java 開(kāi)發(fā)人員的 Ajax 系列,向您展示 GWT 能做什么,并幫助您確定它是否適合您。
GWT(請(qǐng)參閱 參考資料)采用了一種不尋常的方式進(jìn)行 Web 應(yīng)用程序開(kāi)發(fā)。它沒(méi)有采用客戶端和服務(wù)器端代碼庫(kù)的普通隔離,而是提供了一個(gè) Java API,該 API 允許創(chuàng)建基于組件的 GUI,然后編譯它們,從而在用戶的 Web 瀏覽器上顯示它們。與一般的 Web 應(yīng)用程序開(kāi)發(fā)體驗(yàn)相比,使用 GWT 更接近于使用 Swing 或 SWT 進(jìn)行開(kāi)發(fā),它還試圖將 HTTP 協(xié)議和 HTML DOM 模型抽象出去。實(shí)際上,應(yīng)用程序最終幾乎總是會(huì)呈現(xiàn)在 Web 瀏覽器中。
GWT 是通過(guò)代碼生成來(lái)實(shí)現(xiàn)這些功能的,它利用其編譯器從客戶端 Java 代碼生成 JavaScript。GWT 支持 java.lang 和 java.util 包的子集,還支持 GWT 自身提供的 API。編譯后的 GWT 應(yīng)用程序由 HTML、XML 和 JavaScript 片段組成。但是,這些片段很難區(qū)分,所以最好把編譯后的應(yīng)用程序當(dāng)成是黑盒子 —— Java 字節(jié)碼的 GWT 等價(jià)物。
在這篇文章中,我將創(chuàng)建一個(gè)簡(jiǎn)單的 GWT 應(yīng)用程序,用該程序從遠(yuǎn)程 Web API 獲得天氣報(bào)告,并在瀏覽器中顯示它。在整個(gè)過(guò)程中,我將簡(jiǎn)要介紹盡可能多的 GWT 功能,還將提到一些可能遇到的潛在問(wèn)題。
從簡(jiǎn)單的開(kāi)始
清單 1 顯示了可以用 GWT 制作的最簡(jiǎn)單的應(yīng)用程序的 Java 源代碼:
清單 1. 最簡(jiǎn)單的 GWT 示例
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);
}
}
|
這個(gè)代碼看起來(lái)非常像使用 Swing、AWT 或 SWT 編寫的 GUI 代碼。不出所料,清單 1 創(chuàng)建了一個(gè)按鈕,在單擊此按鈕時(shí)會(huì)顯示消息 “Hello World!”。該按鈕被添加到 RootPanel ,這是一個(gè)環(huán)繞 HTML 頁(yè)面主體的 GWT 包裝對(duì)象。圖 1 顯示了應(yīng)用程序在 GWT Shell 中運(yùn)行時(shí)的情況。GWT Shell 是一個(gè)包含在 GWT SDK 中的調(diào)試宿主環(huán)境(debugging hosting environment),與一個(gè)簡(jiǎn)單的瀏覽器組合在一起。
圖 1. 運(yùn)行最簡(jiǎn)單的 GWT 示例
構(gòu)建 Weather Reporter 應(yīng)用程序
我將用 GWT 創(chuàng)建一個(gè)簡(jiǎn)單的 Weather Reporter 應(yīng)用程序。該應(yīng)用程序的 GUI 向用戶顯示了一個(gè)用于輸入 ZIP 代碼的輸入框,還顯示了一個(gè)使用攝氏溫度還是華氏溫度來(lái)表示溫度的選項(xiàng)。當(dāng)用戶單擊 Submit 按鈕時(shí),該應(yīng)用程序用 Yahoo! 的免費(fèi)天氣 API 獲得所選定地區(qū)的 RSS 格式的報(bào)告。然后提取這個(gè)文檔的 HTML 部分,并將它顯示給用戶。
GWT 應(yīng)用程序被打包成模塊,并且必須符合特定的結(jié)構(gòu)。名為 module-name.gwt.xml 的配置文件定義了充當(dāng)應(yīng)用程序入口點(diǎn)的類,并指明是否要從其他 GWT 模塊繼承資源。在應(yīng)用程序的源包結(jié)構(gòu)中,必須將配置文件放在與 client 包和 public 目錄相同的級(jí)別上,所有客戶端 Java 代碼都在 client 包中,而 public 目錄包含項(xiàng)目的 Web 資源,比如圖片、CSS 和 HTML。最后,public 目錄中必須包含一個(gè) HTML 文件,該文件中必須有一個(gè)包含模塊的限定名稱的 meta 標(biāo)記。GWT 的運(yùn)行時(shí) JavaScript 庫(kù)使用這個(gè)文件來(lái)初始化應(yīng)用程序。
在指定了入口點(diǎn)類的情況下,GWT 的 applicationCreator 會(huì)替您生成這個(gè)基本結(jié)構(gòu)。所以可以將調(diào)用
applicationCreator developerworks.gwt.weather.client.Weather 生成一個(gè)項(xiàng)目框架作為創(chuàng)建 Weather Reporter 應(yīng)用程序的起點(diǎn)。在該應(yīng)用程序的源代碼下載中包含的 Ant 構(gòu)建文件中,有一些有用的目標(biāo)(target),可使用它們讓 GWT 項(xiàng)目符合這個(gè)結(jié)構(gòu)。(請(qǐng)參閱 下載)。
開(kāi)發(fā)基本的 GUI
首先,我將開(kāi)發(fā)應(yīng)用程序的用戶界面小部件(widget)的基本布局,且不添加其他任何行為。Widget 類是可以呈現(xiàn)在 GWT UI 中的幾乎所有類的超類。Widget 總是包含在 Panel 中,Panel 本身也是 Widget ,所以可以被嵌套。不同類型的面板提供了不同的布局行為。所以,GWT Panel 扮演的角色與 AWT/Swing 中的 Layout 或 XUL 中的 Box 類似。
所有小部件和面板最終都要附加到包含它們的 Web 頁(yè)面上。如 清單 1 所示,可以直接把它們附加到 RootPanel 上。或者,可以用 RootPanel 獲得對(duì)使用 ID 或類名標(biāo)識(shí)的 HTML 元素的引用。在這個(gè)示例中,我將使用兩個(gè)獨(dú)立的 HTML DIV 元素,它們的名稱分別是 input-container 和 output-container 。第一個(gè)元素包含 Weather Reporter 應(yīng)用程序的 UI 控件,第二個(gè)元素顯示天氣報(bào)告本身。
清單 2 顯示了設(shè)置基本布局所需的代碼;它應(yīng)當(dāng)是自解釋的。HTML 小部件只是 HTML 標(biāo)記的容器,來(lái)自 Yahoo! 天氣種子(weather feed)的 HTML 輸出將顯示在這里。這些代碼都位于 Weather 類的 onModuleLoad() 方法中,這個(gè)方法由 EntryPoint 接口提供。在將包含天氣模塊的 Web 頁(yè)面裝入客戶機(jī)的 Web 瀏覽器時(shí),將調(diào)用這個(gè)方法。
清單 2. Weather Reporter 應(yīng)用程序的布局代碼
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 中呈現(xiàn)的布局:
圖 2. 基本 GUI 布局
用 CSS 添加樣式
呈現(xiàn)的 Web 頁(yè)面看起來(lái)很傻,所以它將從 CSS 樣式規(guī)則中汲取一些優(yōu)點(diǎn)。可以用兩種方式為 GWT 應(yīng)用程序添加樣式。首先,默認(rèn)情況下,每個(gè)小部件都有一個(gè) CSS 類名,其形式為 project-widget 。例如,gwt-Button 和 gwt-RadioButton 是兩個(gè)核心 GWT 小部件類名。面板通常被實(shí)現(xiàn)為一堆嵌套式表格,所以沒(méi)有默認(rèn)的類名。
每個(gè)小部件類型一個(gè)類名(classname-per-widget-type)的默認(rèn)方法使得在整個(gè)應(yīng)用程序中一致地設(shè)置小部件樣式變得非常容易。當(dāng)然,普通的 CSS 選擇器規(guī)則也可以應(yīng)用,所以可以根據(jù)小部件的上下文,用選擇器規(guī)則在同一小部件上應(yīng)用不同的樣式。要得到更多的靈活性,則可以調(diào)用小部件的 setStyleName() 和 addStyleName() 方法,臨時(shí)替換和增加小部件的默認(rèn)類名。
清單 3 組合了這些方法,把樣式應(yīng)用到 Weather Reporter 應(yīng)用程序的輸入面板上。通過(guò)對(duì) inputPanel.setStyleName("weather-input-panel"); 的調(diào)用,在 Weather.java 中創(chuàng)建了 weather-input-panel 類名。
清單 3. 將 CSS 樣式應(yīng)用到 Weather Reporter 應(yīng)用程序的輸入面板
/* 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 顯示了應(yīng)用程序被替換成這些樣式之后的情況:
圖 3. 應(yīng)用了這些樣式之后的輸入面板
添加客戶端行為
現(xiàn)在應(yīng)用程序的基本布局和樣式已經(jīng)就緒,我將開(kāi)始實(shí)現(xiàn)一些客戶端行為。可以用熟悉的偵聽(tīng)器模式在 GWT 中執(zhí)行事件處理。GWT 為鼠標(biāo)事件、鍵盤事件、修改事件等提供了 Listener 接口,還提供了幾個(gè)適配器和助手類,以獲得更多方便。
一般情況下使用 Swing 程序員熟悉的內(nèi)部類形式來(lái)添加事件偵聽(tīng)器。但是,所有 GWT Listener 方法的第一個(gè)參數(shù)都是事件的發(fā)送者,通常是用戶剛剛與之交互的小部件。這意味著可以把同一個(gè) Listener 實(shí)例附加到所需的多個(gè)小部件上;可以用 sender 參數(shù)確定是哪個(gè)小部件觸發(fā)了事件。
清單 4 顯示了 Weather Reporter 應(yīng)用程序中實(shí)現(xiàn)的兩個(gè)事件偵聽(tīng)器。click 句柄被添加到了 Submit 按鈕上,keyhandler 被添加到了 TextBox 上。不管是單擊 Submit 按鈕,還是在 TextBox 擁有焦點(diǎn)時(shí)按下回車鍵,都會(huì)導(dǎo)致相關(guān)的句柄調(diào)用私有的 validateAndSubmit() 方法。在添加到清單 4 的代碼中之后,txBox 和 ucRadio 已經(jīng)成為 Weather 類的實(shí)例變量,所以可以從驗(yàn)證方法訪問(wèn)它們。
清單 4. 添加客戶端行為
// Create Submit button, with click listener inner class attached
Button 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 focus
txBox.addKeyboardListener(new KeyboardListenerAdapter(){
public void onKeyPress(Widget sender, char keyCode, int modifiers) {
// Check for Enter key
if ((keyCode == 13) && (modifiers == 0)) {
validateAndSubmit();
}
}
});
|
清單 5 顯示了 validateAndSubmit() 方法的實(shí)現(xiàn)。該實(shí)現(xiàn)非常簡(jiǎn)單,由封裝驗(yàn)證邏輯的 ZipCodeValidator 類完成。如果用戶沒(méi)有輸入正確的 5 位數(shù)字的 ZIP 代碼,那么 validateAndSubmit() 將在警告框中顯示錯(cuò)誤消息,如果這種情況出現(xiàn)在 GWT 中,則會(huì)調(diào)用 Window.alert() 。如果 ZIP 代碼正確,那么它將與用戶對(duì)攝氏或華氏溫度單位的選擇一起被傳遞給 fetchWeatherHtml() 方法,這個(gè)方法稍后再介紹。
清單 5. validateAndSubmit 邏輯
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 進(jìn)行客戶端調(diào)試
在這里我要岔開(kāi)一會(huì),提一下 GWT Shell,它擁有允許在 Java IDE 中調(diào)試客戶端代碼的 JVM 掛鉤。您可以與 Web UI 進(jìn)行交互,分步調(diào)試表示客戶端執(zhí)行的相應(yīng) JavaScript 代碼的 Java 代碼。這是一項(xiàng)很重要的功能,因?yàn)樵诳蛻舳松险{(diào)試所生成的 JavaScript 基本上是不可能的。
可以很容易地配置一個(gè) Eclipse 調(diào)試任務(wù),從而通過(guò) com.google.gwt.dev.GWTShell 類啟動(dòng) GWT Shell。圖 4 顯示了按下 Submit 按鈕后,在 validateAndSubmit() 方法的斷點(diǎn)處暫停的 Eclipse:
圖 4. 調(diào)試客戶端 GWT 代碼的 Eclipse
與服務(wù)器端組件進(jìn)行通信
現(xiàn)在 Weather Reporter 應(yīng)用程序就可以搜集和驗(yàn)證用戶輸入了。下一步是從服務(wù)器中檢索數(shù)據(jù)。在正常的 Ajax 開(kāi)發(fā)中,需要直接從 JavaScript 調(diào)用服務(wù)器端資源,并接收編碼成 JavaScript Object Notation(JSON)或 XML 的數(shù)據(jù)。GWT 在自己的遠(yuǎn)程過(guò)程調(diào)用(remote procedure call,RPC)機(jī)制背后抽象這個(gè)通信過(guò)程。
在 GWT 的術(shù)語(yǔ)中,客戶機(jī)代碼與運(yùn)行在 Web 服務(wù)器上的服務(wù) 進(jìn)行通信。用來(lái)公開(kāi)這些服務(wù)的 RPC 機(jī)制與 Java RMI 使用的方法類似。這意味著只需要編寫服務(wù)的服務(wù)器端實(shí)現(xiàn)和兩個(gè)接口即可。代碼生成和反射將負(fù)責(zé)處理客戶機(jī)存根和服務(wù)器端主干代理(server-side skeleton proxies)。
相應(yīng)地,要做的第一步是定義 Weather Reporter 服務(wù)的接口。這個(gè)接口必須擴(kuò)展 GWT RemoteService 接口,它包含應(yīng)該公開(kāi)給 GWT 客戶機(jī)代碼的服務(wù)方法的簽名。因?yàn)?GWT 中的 RPC 調(diào)用是在 JavaScript 代碼和 Java 代碼之間進(jìn)行的,所以 GWT 集成了對(duì)象序列化機(jī)制,用它來(lái)協(xié)調(diào)跨語(yǔ)言分界(language divide)的參數(shù)和返回值(請(qǐng)參閱 可序列化類型 側(cè)欄,了解您可以使用哪些可序列化類型)。
 |
可序列化類型
GWT 下可序列化類型的簡(jiǎn)要概括如下:
- 基本類(例如
int )和基本包裝對(duì)象類(例如 Integer )是可序列化的。
String 和 Date 是可序列化的。
- 可序列化類型的數(shù)組本身是可序列化的。
- 如果用戶自定義類的所有持久性成員是可序列化的,而且用戶自定義類實(shí)現(xiàn)了 GWT 的
IsSerializable 接口,那么自定義類是可序列化的。
Collection 類可以與 Javadoc 注釋結(jié)合使用,通過(guò)注釋聲明它們包含的可序列化類型。
因?yàn)榭蛻魴C(jī)代碼被限制在 GWT 實(shí)現(xiàn)的 Java 類的一個(gè)很小的子集上,所以這些可序列化類型的覆蓋面相當(dāng)廣泛。
|
|
定義了服務(wù)接口之后,下一步就是在擴(kuò)展 GWT 的 RemoteServiceServlet 類的類中實(shí)現(xiàn)該接口。顧名思義,這是 Java 語(yǔ)言的 HttpServlet 的一個(gè)具體類,所以可以將它放在任何 servlet 容器中。
這里值得一提的一個(gè) GWT 特性是:服務(wù)的遠(yuǎn)程接口必須位于應(yīng)用程序的 client 包中,因?yàn)樾枰獙⑺傻?JavaScript 的生成過(guò)程中。但是,因?yàn)榉?wù)器端實(shí)現(xiàn)類引用了遠(yuǎn)程接口,所以現(xiàn)在在服務(wù)器端和客戶機(jī)代碼之間存在一個(gè) Java 編譯時(shí)依賴項(xiàng)。對(duì)于這個(gè)問(wèn)題,我的解決方案是將遠(yuǎn)程接口放在 client 的 common 子包中。然后在 Java 構(gòu)建中包含 common 包,但不包含 client 包中的剩余部分。這可以確保客戶機(jī)代碼生成的類文件只是那些需要轉(zhuǎn)換成 JavaScript 的文件。更好的解決方案是將包結(jié)構(gòu)分解成兩個(gè)源目錄,一個(gè)負(fù)責(zé)客戶端代碼,一個(gè)負(fù)責(zé)服務(wù)器端代碼,然后將公共類復(fù)制到兩個(gè)目錄中。
清單 6 顯示了 Weather Reporter 應(yīng)用程序使用的遠(yuǎn)程服務(wù)接口 WeatherService 。它接受 ZIP 代碼和攝氏/華氏標(biāo)記作為輸入,返回包含 HTML 天氣描述的 String 。清單 6 顯示了 YahooWeatherServiceImpl 的框架,它使用 Yahoo! 的天氣 API 獲得給定 ZIP 代碼的 RSS 天氣種子,并從中獲得 HTML 描述。
清單 6. 遠(yuǎn)程 WeatherService 接口和部分實(shí)現(xiàn)
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
}
}
|
從這時(shí)起,就開(kāi)始脫離標(biāo)準(zhǔn)的 RMI 方法。因?yàn)閬?lái)自 JavaScript 的 Ajax 調(diào)用是異步的,所以需要做些額外的工作來(lái)定義客戶機(jī)代碼用來(lái)調(diào)用服務(wù)的異步接口。異步接口的方法簽名與遠(yuǎn)程接口的方法簽名有所不同,所以 GWT 要依靠 Magical Coincidental Naming。換句話說(shuō),在異步接口和遠(yuǎn)程接口之間不存在靜態(tài)的編譯時(shí)關(guān)系,但是 GWT 會(huì)通過(guò)命名約定來(lái)指出該關(guān)系。清單 7 顯示了 WeatherService 的異步接口:
清單 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);
}
|
可以看到,一般的想法是創(chuàng)建叫做 MyServiceAsync 的接口,并提供與每個(gè)方法簽名對(duì)等的事物,然后刪除所返回類型,添加類型為 AsyncCallback 的額外參數(shù)。異步接口必須放在與遠(yuǎn)程接口相同的包中。AsyncCallback 類有兩個(gè)方法:onSuccess() 和 onFailure() 。如果對(duì)服務(wù)的調(diào)用成功,則用服務(wù)調(diào)用的返回值調(diào)用 onSuccess() 。如果遠(yuǎn)程調(diào)用失敗,則調(diào)用 onFailure() ,并傳遞由該服務(wù)生成的 Throwable ,以表示失敗的原因。
從客戶機(jī)調(diào)用服務(wù)
有了 WeatherService 和它的異步接口之后,現(xiàn)在就可以修改 Weather Reporter 客戶機(jī),從而調(diào)用服務(wù)并處理服務(wù)器響應(yīng)。第一步只是公式化地設(shè)置代碼:通過(guò)調(diào)用 GWT.create(WeatherService.class) 并向下傳送所返回的對(duì)象,創(chuàng)建一個(gè)在 Weather 客戶機(jī)上使用的 WeatherServiceAsync 實(shí)例。接下來(lái),必須將 WeatherServiceAsync 強(qiáng)行轉(zhuǎn)換成 ServiceDefTarget ,這樣才能在它上面調(diào)用 setServiceEntryPoint() 。setServiceEntryPoint() 指向?qū)?yīng)的遠(yuǎn)程服務(wù)實(shí)現(xiàn)所部署的 URL 上的 WeatherServiceAsync 存根。請(qǐng)注意,這實(shí)際上是在編譯時(shí)硬編碼的。因?yàn)檫@個(gè)代碼成為在 Web 瀏覽器中部署的 JavaScript,所以沒(méi)辦法在運(yùn)行時(shí)從屬性文件中查找這個(gè) URL。顯然,這限制了編譯后的 GWT Web 應(yīng)用程序的移植性。
清單 8 顯示了 WeatherServiceAsync 對(duì)象的設(shè)置,然后給出了 fetchWeatherHtm() 的實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)我在前面提到過(guò)(請(qǐng)參閱 添加客戶端行為):
清單 8. 使用 RPC 調(diào)用遠(yuǎn)程服務(wù)
// Statically configure RPC service
private 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);
}
});
}
|
對(duì)服務(wù)的 getWeatherHtml() 的實(shí)際調(diào)用實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單:使用一個(gè)匿名回調(diào)句柄類將服務(wù)器的響應(yīng)傳遞給顯示響應(yīng)的方法即可。
圖 5 顯示了應(yīng)用程序的運(yùn)行情況,顯示了從 Yahoo! 天氣 API 檢索的天氣報(bào)告:
圖 5. Weather Reporter 應(yīng)用程序顯示了從 Yahoo! 得到的報(bào)告
服務(wù)器端驗(yàn)證的需要
用 GWT 合并客戶端和服務(wù)器端代碼存在內(nèi)在危險(xiǎn)。因?yàn)槟褂?Java 語(yǔ)言來(lái)編寫所有代碼,所以 GWT 的抽象隱藏了客戶機(jī)/服務(wù)器之間的分離,很容易讓人誤認(rèn)為可以相信運(yùn)行時(shí)的客戶端代碼。這是錯(cuò)誤的。Web 瀏覽器上運(yùn)行的任何代碼都可能被惡意用戶篡改或者完全繞開(kāi)。GWT 提供了高層次的混淆,從而可以將這個(gè)問(wèn)題降低到一定程度,但是仍然存在次要攻擊點(diǎn):GWT 客戶機(jī)及其服務(wù)之間的 HTTP 通信量。
假設(shè)我是一個(gè)攻擊者,想利用 Weather Reporter 應(yīng)用程序的弱點(diǎn)。圖 6 顯示了 Microsoft 的 Fiddler 工具,它攔截了從 Weather Reporter 客戶機(jī)到運(yùn)行在服務(wù)器之上的 WeatherService 的請(qǐng)求。攔截到請(qǐng)求之后,F(xiàn)iddler 允許對(duì)請(qǐng)求的任意部分進(jìn)行修改。高亮的文本顯示了我找到的指定 ZIP 代碼在請(qǐng)求中的編碼位置。現(xiàn)在我可以將 ZIP 代碼更改為任何我喜歡的值,大致范圍是從 “10001” 到 “XXXXX”。
圖 6. 用 Fiddler 繞開(kāi)客戶端驗(yàn)證
現(xiàn)在,假設(shè) YahooWeatherServiceImpl 中有一些服務(wù)器端代碼對(duì) ZIP 代碼調(diào)用了 Integer.parseInt() 。ZIP 代碼最終一定會(huì)通過(guò)集成到 Weather 的 validateAndSubmit() 方法中的驗(yàn)證檢查。正如已經(jīng)看到的那樣,這個(gè)檢查已經(jīng)被破壞,拋出了一個(gè) NumberFormatException 。
在這個(gè)示例中,沒(méi)有發(fā)生什么可怕的事情,攻擊者只是在客戶機(jī)上看到了一條錯(cuò)誤消息。但是,對(duì)于處理更敏感數(shù)據(jù)的 GWT 應(yīng)用程序進(jìn)行全面攻擊也是有可能的。假設(shè) ZIP 代碼被替換成了訂單跟蹤應(yīng)用程序中的客戶 ID 號(hào)碼。攔截和修改這個(gè)值可能暴露其他客戶的敏感財(cái)務(wù)信息。在數(shù)據(jù)庫(kù)查詢可以使用數(shù)據(jù)值的任何地方,同樣的方式都有可能導(dǎo)致 SQL 注入攻擊。
對(duì)于以前曾經(jīng)使用過(guò) Ajax 應(yīng)用程序的人來(lái)說(shuō),這些不應(yīng)是天方夜譚。只需要雙擊任何輸入值,就可以在服務(wù)器上重新驗(yàn)證它們。關(guān)鍵是要記住:在 GWT 應(yīng)用程序中編寫的一些 Java 代碼在運(yùn)行時(shí)實(shí)際上是不可信任的。但是,確實(shí)還有一線希望可以解決這個(gè) GWT 問(wèn)題。在 Weather Reporter 應(yīng)用程序中,我編寫了一個(gè)在客戶機(jī)上使用的 ZipCodeValidator ,可以將它移入 client.common 包,并在服務(wù)器端重用相同的驗(yàn)證。清單 9 顯示了集成到 YahooWeatherServiceImpl 中的這個(gè)檢查程序:
清單 9. 集成到 YahooWeatherServiceImpl 中的 ZipCodeValidator
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 調(diào)用本機(jī) JavaScript
可視效果庫(kù)在 Web 應(yīng)用程序開(kāi)發(fā)中變得越來(lái)越流行,不論它們的效果只是用來(lái)提供細(xì)微的用戶交互線索還是僅僅用于裝飾。我想給 Weather Reporter 應(yīng)用程序添加一些吸引眼球的東西。GWT 沒(méi)有提供這類功能,但是它的 JavaScript 本機(jī)接口(JSNI)提供了解決方案。JSNI 允許直接在 GWT 客戶機(jī) Java 代碼中進(jìn)行 JavaScript 調(diào)用。這意味著我可以利用來(lái)自 Scriptaculous 庫(kù)的效果(請(qǐng)參閱 參考資料)或來(lái)自 Yahoo! 用戶界面庫(kù)的效果。
JSNI 巧妙地把 Java 語(yǔ)言的 native 關(guān)鍵字和嵌入特殊注釋塊中的 JavaScript 組合在一起。用示例對(duì)此進(jìn)行解釋可能是最好的方法,所以清單 10 顯示了一個(gè)方法,該方法調(diào)用了 Element 上的指定 Scriptaculous 效果:
清單 10. 用 JSNI 調(diào)用 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 代碼,因?yàn)榫幾g器只看到 private native void applyEffect(Element element, String effectName); 。GWT 將解析注釋塊的內(nèi)容,并逐字地輸出 JavaScript。GWT 提供了 $wnd 和 $doc 變量,它們分別代表窗口和文檔對(duì)象。在這個(gè)示例中,我只是訪問(wèn)頂級(jí) Scriptaculous Effect 對(duì)象,并用 JavaScript 的方括號(hào)對(duì)象存取器語(yǔ)法調(diào)用調(diào)用方指定的命名函數(shù)。Element 類型是 GWT 提供的 “魔法” 類型,它在 Java 和 JavaScript 代碼中都代表 Widget 的底層 HTML DOM 元素。String 是可以通過(guò) JSNI 在 Java 代碼和 JavaScript 之間透明傳遞的少數(shù)類型之一。
現(xiàn)在我有了一個(gè)天氣報(bào)告,當(dāng)數(shù)據(jù)從服務(wù)器返回時(shí),該天氣報(bào)告逐漸淡化消失。最后一項(xiàng)操作是在效果完成時(shí)重新啟用 ZIP 代碼 TextBox 。Scriptaculous 使用異步回調(diào)機(jī)制把特殊的生命周期通知給偵聽(tīng)器。在這里,事情變得稍微有點(diǎn)復(fù)雜,因?yàn)槲倚枰ㄟ^(guò)回調(diào) JavaScript 使它回到 GWT 客戶機(jī)的 Java 代碼中。在 JavaScript 中,可以用任意數(shù)量的參數(shù)調(diào)用函數(shù),所以 Java 風(fēng)格的方法重載已不存在。這意味著 JSNI 需要使用一個(gè)笨拙的語(yǔ)法來(lái)引用 Java 方法,以消除可能的重載歧義。GWT 文檔是這樣說(shuō)明這個(gè)語(yǔ)法的:
[instance-expr.]@class-name::method-name(param-signature)(arguments)
|
instance-expr. 部分是可選的,因?yàn)殪o態(tài)方法被調(diào)用時(shí)不需要對(duì)象引用。同樣,用示例來(lái)查看它的效果是最容易的,如清單 11 所示:
清單 11. 用 JSNI 回調(diào) Java 代碼
/**
* 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() 方法已經(jīng)被更改為將額外的 afterFinish 參數(shù)傳遞給 Scriptaculous。afterFinish 的值是一個(gè)匿名函數(shù),在效果完成時(shí)被調(diào)用。這與 GWT 事件句柄中使用的內(nèi)部類的概念有點(diǎn)相似。對(duì) Java 代碼實(shí)際進(jìn)行回調(diào)時(shí),要指定將在該代碼上激活調(diào)用的 Weather 對(duì)象,然后指定 Weather 類的完整規(guī)范名稱,這之后是指定將要調(diào)用的函數(shù)的名稱。第一對(duì)空的括號(hào)指明將調(diào)用不帶參數(shù)的 effectFinished() 方法。第二對(duì)括號(hào)調(diào)用函數(shù)。
這里的秘訣在于:本地變量 weather 保存了 this 引用的一個(gè)副本。根據(jù) JavaScript 調(diào)用語(yǔ)義操作的方式,afterFinish 函數(shù)中的 this 變量實(shí)際上是一個(gè) Scriptaculous 對(duì)象,因?yàn)閷⒂?Scriptaculous 進(jìn)行這個(gè)函數(shù)調(diào)用。請(qǐng)?jiān)诜庋b之外做一份 this 的副本,這是一項(xiàng)簡(jiǎn)單的工作。
現(xiàn)在已經(jīng)演示了 JSNI 的一些功能,還應(yīng)當(dāng)指出的是,把 Scriptaculous 集成到 GWT 的更佳方式是將 Scriptaculous 效果功能包裝成定制的 GWT 小部件。這正是 Alexei Sokolov 在 GWT 組件庫(kù)中所做的工作(請(qǐng)參閱 參考資料)。
現(xiàn)在就完全完成了 Weather Reporter 應(yīng)用程序,我將回顧一下用 GWT 進(jìn)行 Web 開(kāi)發(fā)的優(yōu)缺點(diǎn)。
為什么使用 GWT?
與您的預(yù)期可能有所不同,GWT 應(yīng)用程序顯然不太類似于 Web 應(yīng)用程序。GWT 實(shí)際上把瀏覽器作為輕量級(jí) GUI 應(yīng)用程序的運(yùn)行時(shí)環(huán)境,結(jié)果,使用 GWT 進(jìn)行開(kāi)發(fā)更接近于使用 Morfik、OpenLaszlo 甚至 Flash 進(jìn)行開(kāi)發(fā),而不太像是一般的 Web 應(yīng)用程序開(kāi)發(fā)。所以,GWT 最適合的 Web 應(yīng)用程序是能夠作為單一頁(yè)面的豐富 Ajax GUI 存在的應(yīng)用程序。Google 最近的一些 beta 發(fā)行版(如日歷和電子表應(yīng)用程序)都具有這樣的特性,這可能不是什么巧合。它們是一些很棒的應(yīng)用程序,但是不能用這種方式解決所有的業(yè)務(wù)場(chǎng)景。大多數(shù) Web 應(yīng)用程序非常適合以頁(yè)面為中心的模型,而 Ajax 允許在需要的地方使用更豐富的交互范例。GWT 不太適合傳統(tǒng)的以頁(yè)面為中心的應(yīng)用程序。雖然可以把 GWT 小部件與普通的 HTML 表單輸入組合,但 GWT 小部件的狀態(tài)與頁(yè)面的其他部分是分開(kāi)的。例如,沒(méi)有某種簡(jiǎn)單的方法可以把 GWT Tree 小部件中選定的值作為普通表單的一部分一起提交。
 |
許可
GWT 的運(yùn)行庫(kù)在 Apache License 2.0 下授權(quán),可以免費(fèi)使用 GWT 創(chuàng)建商業(yè)應(yīng)用程序。但是,GWT 工具鏈只以二進(jìn)制形式提供,且不允許修改。該工具鏈中包括 Java-to-JavaScript 編譯器。這意味著生成的 JavaScript 中的任何錯(cuò)誤都超出了您的控制。一個(gè)特殊問(wèn)題是 GWT 對(duì)用戶代理檢測(cè)的依賴:新發(fā)行的每個(gè)瀏覽器都需要對(duì) GWT 工具箱進(jìn)行更新,以提供支持。
|
|
如果決定把 GWT 用于 J2EE 應(yīng)用程序環(huán)境,那么 GWT 的設(shè)計(jì)會(huì)使集成變得相對(duì)簡(jiǎn)單。在這個(gè)場(chǎng)景中,GWT 服務(wù)應(yīng)該被當(dāng)成與 Struts 中的 Action 類似的東西 —— 一個(gè)很薄的中間層,它只代理對(duì)后端業(yè)務(wù)邏輯調(diào)用的 Web 請(qǐng)求。因?yàn)?GWT 服務(wù)就是 HTTP servlet,所以可以容易地將它集成到 Struts 或 SpringMVC 中,例如放在身份驗(yàn)證過(guò)濾器后面。
GWT 確實(shí)有一些非常顯眼的缺陷。首先,它缺乏對(duì)功能退化的預(yù)防。現(xiàn)代 Web 應(yīng)用程序開(kāi)發(fā)中的最佳實(shí)踐是創(chuàng)建沒(méi)有 JavaScript 的頁(yè)面,然后在可以使用 JavaScript 的地方用它修飾和添加額外的行為。在 GWT 中,如果 JavaScript 不可用,則根本得不到 UI。對(duì)于某些 Web 應(yīng)用程序類型來(lái)說(shuō),這簡(jiǎn)直是不可接受的。國(guó)際化也是 GWT 的一個(gè)主要問(wèn)題。因?yàn)?GWT 客戶機(jī) Java 類在瀏覽器中運(yùn)行,所以不能通過(guò)在運(yùn)行時(shí)訪問(wèn)屬性或資源綁定來(lái)得到本地化的字符串。現(xiàn)在有一個(gè)復(fù)雜的工作區(qū),它需要為每個(gè)地區(qū)創(chuàng)建的客戶端類的子類(請(qǐng)參閱 參考資料),但是 GWT 的工程師正在開(kāi)發(fā)更可行的解決方案。
在代碼生成的情形中
GWT 架構(gòu)中最具爭(zhēng)議的問(wèn)題可能就是在客戶端代碼中對(duì) Java 語(yǔ)言的切換。有些 GWT 的擁護(hù)者認(rèn)為用 Java 語(yǔ)言編寫客戶端代碼實(shí)際上要比編寫 JavaScript 好。并不是所有人都贊成這個(gè)觀點(diǎn),許多 JavaScript 程序員極不情愿犧牲他們語(yǔ)言的靈活性和表現(xiàn)力,來(lái)獲得有時(shí)非常繁重的 Java 開(kāi)發(fā)工作。用 Java 代碼代替 JavaScript 比較有吸引力的一種情況就是:團(tuán)隊(duì)缺少有經(jīng)驗(yàn)的 Web 開(kāi)發(fā)人員。但是,如果團(tuán)隊(duì)正在轉(zhuǎn)向 Ajax 開(kāi)發(fā),那么最好是雇傭有經(jīng)驗(yàn)的 JavaScript 程序員,而不要依靠 Java 程序員利用私有的工具生成混亂的 JavaScript。由于 GWT 擴(kuò)展到 JavaScript、HTTP 和 HTML 的漏洞所導(dǎo)致的 bug 是不可避免的,所以缺乏經(jīng)驗(yàn)的 Web 程序員要花很長(zhǎng)時(shí)間跟蹤它們。作為開(kāi)發(fā)人員和博客,Dimitri Glazkov 指出:“如果不能處理 JavaScript,就不應(yīng)當(dāng)編寫 Web 應(yīng)用程序的代碼。HTML、CSS 和 JavaScript 是這條路上的三個(gè)必備條件。”(請(qǐng)參閱 參考資料)。
有些人認(rèn)為因?yàn)橛辛遂o態(tài)類型化和編譯時(shí)檢測(cè),Java 編碼天生就比 JavaScript 編程不容易出錯(cuò)。這是一個(gè)相當(dāng)靠不住的論調(diào)。用任何語(yǔ)言都可能編寫糟糕的代碼,大量充滿 bug 的 Java 應(yīng)用程序就是證明。也可以依靠 GWT 的代碼生成來(lái)消除 bug。但是,離線語(yǔ)法檢測(cè)和客戶端代碼的驗(yàn)證無(wú)疑會(huì)帶來(lái)一些好處。JavaScript 也可以用 Douglas Crockford 的 JSLint 的形式得到它(請(qǐng)參閱 參考資料)。GWT 在單元測(cè)試上有優(yōu)勢(shì),可以為客戶端代碼提供 JUnit 集成。單元測(cè)試支持仍然是 JavaScript 很欠缺的一個(gè)領(lǐng)域。
在開(kāi)發(fā) Weather Reporter 應(yīng)用程序時(shí),我發(fā)現(xiàn)客戶端 Java 代碼最引人注目的情況是它在兩個(gè)層上共享一些驗(yàn)證類的能力。這顯然減少了開(kāi)發(fā)勞動(dòng)。跨 RPC 傳遞的任何類都適用這種情況;只需要編碼一次,就可以將它們用在客戶機(jī)和服務(wù)器代碼中。不幸的是,抽象是有漏洞的:例如,在我的 ZIP 代碼驗(yàn)證器中,本想使用正則表達(dá)式執(zhí)行檢測(cè)。但是,GWT 沒(méi)有實(shí)現(xiàn) String.match() 方法。而且即使它實(shí)現(xiàn)了這個(gè)方法,在將 GWT 中的正則表達(dá)式部署到客戶機(jī)和服務(wù)器代碼時(shí),也存在語(yǔ)義上的差異。這是因?yàn)?GWT 對(duì)宿主環(huán)境底層的正則表達(dá)式機(jī)制的依賴也是不完美抽象所帶來(lái)問(wèn)題的一個(gè)例子。
GWT 非常被看好的一個(gè)重要原因是它的 RPC 機(jī)制和內(nèi)置在 Java 代碼和 JavaScript 之間的對(duì)象序列化。這消除了普通 Ajax 應(yīng)用程序中可以看到的許多繁重工作。但是,它是有前提的。如果想使用這個(gè)功能而不使用 GWT 的其他部分,那么 Direct Web Remoting(DWR,它用 Java 代碼和 JavaScript 之間的對(duì)象偽裝提供了 RPC 功能)非常值得考慮(請(qǐng)參閱 參考資料)。
對(duì)于將 Ajax 應(yīng)用程序開(kāi)發(fā)的一些低層方面(如跨瀏覽器的不兼容、DOM 事件模型和進(jìn)行 Ajax 調(diào)用)抽象出來(lái),GWT 做得很好。但是現(xiàn)代的 JavaScript 工具包(例如 Yahoo! UI 庫(kù)、Dojo 和 MochiKit)都提供了類似級(jí)別的抽象,卻不需要求助于代碼生成。此外,所有這些工具包都是開(kāi)源的,所以可以對(duì)其進(jìn)行定制,以滿足自己的需求,或者在出現(xiàn) bug 的時(shí)候進(jìn)行修補(bǔ)。對(duì)于黑盒子式的 GWT,這是不可能的。(請(qǐng)參閱 許可 側(cè)欄)。
結(jié)束語(yǔ)
GWT 是一個(gè)全面的框架,提供了許多有用的功能。但是,GWT 并不是萬(wàn)能的,它針對(duì)的只是 Web 應(yīng)用程序開(kāi)發(fā)市場(chǎng)中一個(gè)相對(duì)狹窄的市場(chǎng)。我希望這份簡(jiǎn)要的介紹能讓您對(duì) GWT 的功能和局限性有一定的了解。雖然 GWT 肯定不會(huì)滿足每個(gè)人的需求,但它仍然是一個(gè)主要的技術(shù)成果,在設(shè)計(jì)下一個(gè) Ajax 應(yīng)用程序時(shí),值得認(rèn)真地考慮 GWT。與我在這里介紹的相比,GWT 具有更廣的廣度和更深的深度,所以請(qǐng)閱讀 Google 的文檔,以了解更多內(nèi)容,或者加入 GWT 開(kāi)發(fā)人員論壇上的討論(請(qǐng)參閱 參考資料)。
下載
描述 |
名字 |
大小 |
下載方法 |
GWT Weather Reporter application |
j-ajax4-gwt-weather.zip |
2.1KB |
HTTP |
參考資料
學(xué)習(xí)
獲得產(chǎn)品和技術(shù)
討論
關(guān)于作者
 |
|
 |
Philip McCarthy 是軟件開(kāi)發(fā)顧問(wèn),專攻 Java 和 Web 技術(shù)。他最近參與了惠普實(shí)驗(yàn)室和 Orange 的數(shù)字媒體和電信項(xiàng)目,目前在倫敦市開(kāi)發(fā)財(cái)政軟件。
|
|