Google Web工具包(GWT)確實(shí)是使用Java開發(fā)Ajax應(yīng)用的一種誘人方法。如果你在AWT/Swing/SWT和服務(wù)器小程序方面有著扎實(shí)背景,實(shí)際上很容易學(xué)會(huì)使用GWT,但如果要做的不僅僅是快速原型設(shè)計(jì),那么某些難題仍然存在。
忠告之一: 分而治之
眾所周知,GWT應(yīng)用就是Java應(yīng)用。不過(guò),問(wèn)題在于是“哪種Java”,我們需要牢記: GWT編譯的是與J2SE 1.4.2或者更早版本兼容的Java源代碼。另外,只有J2SE 1.4.2 API的子集得到支持,即java.lang和java.util程序包。即便在使用這些程序包時(shí),也要非常認(rèn)真地研究Google在運(yùn)行庫(kù)支持方面的注 釋,并且牢記相應(yīng)的忠告: 如果確保從一開始就只使用客戶端代碼中的可轉(zhuǎn)換類,那么就可以避免許多問(wèn)題。為了及早發(fā)現(xiàn)問(wèn)題,只要在宿主模式(hosted mode)下運(yùn)行,就要對(duì)照J(rèn)RE仿真庫(kù)檢驗(yàn)代碼。因而,第一次運(yùn)行應(yīng)用時(shí),就會(huì)發(fā)現(xiàn)大部分不支持的庫(kù)。所以,要及早并且經(jīng)常運(yùn)行。
現(xiàn)在,筆者給出的忠告就是“分而治之”,具體意思就是一開始就把應(yīng)用代碼分成三個(gè)不同的部分: 客戶端代碼、RPC相關(guān)代碼和服務(wù)器端代碼,然后構(gòu)建相應(yīng)的Eclipse項(xiàng)目,從而完成任務(wù)。這樣一來(lái),就可以利用不同的Java語(yǔ)言版本,用于客戶端 和服務(wù)器部分。筆者用Java 5構(gòu)建了應(yīng)用的服務(wù)器部分(服務(wù)器小程序代碼); 但如果使用Mustang版本,那么在本文的代碼片段中(由于篇幅有限,本文所涉及的程序代碼可通過(guò)以下鏈接查詢: http://blog.ccw.com.cn/article-htm-itemid-17924-type-blog.html),可以用Java 6取代Java 5。即便在服務(wù)器端仍然使用J2SE 1.4.2,這種分治法也可以在將來(lái)提供更大的靈活性,明確分離代碼(“分離問(wèn)題”),而不會(huì)在GWT宿主模式下限制調(diào)試操作。如果所有部分都在一個(gè) Eclipse項(xiàng)目中,則需要非常嚴(yán)謹(jǐn),特別是在服務(wù)器端上; 不然,就會(huì)出現(xiàn)編譯或者運(yùn)行問(wèn)題。
需要使用特殊的命名約定,這樣可以清楚確認(rèn)不同項(xiàng)目,并且簡(jiǎn)化部署腳本。可以使用譬如名為GWT-< ModuleName>的Eclipse工作集來(lái)包括所有三個(gè)項(xiàng)目。這里,“ModuleName”是識(shí)別Web應(yīng)用的GWT模塊的名稱。
● 客戶端代碼: 包含與用戶界面相關(guān)的代碼,可以轉(zhuǎn)換成JavaScript。因此,局限于J2SE 1.4.2和GWT運(yùn)行時(shí)支持。啟用每個(gè)項(xiàng)目的Eclipse Java編譯器設(shè)置和“Java編譯器錯(cuò)誤/警告”,把Java依從級(jí)別調(diào)整到1.4、把源代碼和類文件兼容性調(diào)整到1.4(假設(shè)不是使用1.4之前的 JDK版本)。該項(xiàng)目的名稱是< ModuleName>-client,譬如“JUnit2MR-client”,它依賴于構(gòu)建路徑設(shè)置中的< ModuleName>-rpc項(xiàng)目。程序包名稱類似< com.company.project>.gwt.< moduleName>.client。
● RPC相關(guān)代碼: 包含RPC相關(guān)的代碼,可以轉(zhuǎn)換成JavaScript。該項(xiàng)目遵從與上述客戶端代碼項(xiàng)目同樣的指導(dǎo)準(zhǔn)則。項(xiàng)目名是< ModuleName>-rpc,譬如“JUnit2MR-rpc”,它并不依賴于其他任何項(xiàng)目。程序包名稱與< ModuleName>-client項(xiàng)目的程序包名稱一樣。RPC項(xiàng)目包含客戶端上的遠(yuǎn)程接口、RPC期間由GWT進(jìn)行序列化的數(shù)據(jù)傳輸對(duì)象,以 及全局常量類。
● 服務(wù)器端代碼: 含有服務(wù)器小程序代碼,如果服務(wù)器端由Java服務(wù)器小程序組成的話。如果使用Tomcat 5.5或者Tomcat 6,可以充分利用Java 5+的全部功能。啟用每個(gè)項(xiàng)目的Eclipse編譯器設(shè)置,然后使用Java 5編譯器設(shè)置,依從級(jí)別設(shè)置為5.0。如果使用Eclipse 3.2.2,那么其新的“源代碼→清理”特性也值得配置。該項(xiàng)目名稱是< ModuleName>-server,譬如“JUnit2MR-server”,它依賴于構(gòu)建路徑設(shè)置中的< ModuleName>-rpc項(xiàng)目。如果按照GWT的默認(rèn)程序包提案進(jìn)行編程,程序包名稱是< com.company.project>.gwt.< moduleName>.server。
忠告之二: 調(diào)試和錯(cuò)誤報(bào)告不僅僅只有Window.alert ()
在創(chuàng)建GWT應(yīng)用時(shí),其實(shí)可以使用IDE的全部調(diào)試功能。但在深入分析何處可能出現(xiàn)錯(cuò)誤之前,需要代碼的客戶端和 服務(wù)器端都有可靠的異常報(bào)告機(jī)制。使用try/catch代碼塊通常可以做到這一點(diǎn)。在客戶端的catch代碼塊中,應(yīng)當(dāng)注意這一現(xiàn)實(shí): 默認(rèn)的方法調(diào)用e.printStackTrace()并不是在所有情況下都適合的解決辦法。它適用于應(yīng)用運(yùn)行在GWT宿主模式下,把文本輸出到 Eclipse控制臺(tái)。不過(guò)在Web模式下,要問(wèn)問(wèn)自己: “我發(fā)送到stdout或者stderr的堆棧跟蹤信息和錯(cuò)誤信息會(huì)在什么地方顯示?”一種可能的解決方法就是使用Mat Gessel的調(diào)試實(shí)用程序類(http://www.asquare.net/gwttk),但是需要瀏覽器JavaScript控制臺(tái)來(lái)查看Web模 式下的結(jié)果。
在客戶端,建議要做的一件事就是,使用GWT.setUncaughtExceptionHandler()方 法,為任何未被發(fā)現(xiàn)的異常提供自己的異常處理程序。發(fā)現(xiàn)了這幾種異常后,有幾個(gè)選擇: GWT.log(message, caught)、Debug.println (message_with_stacktrace); 如果使用Mat Gessel的Debug類,可選擇Window.alert(message_with_stacktrace),或者自己定制的錯(cuò)誤報(bào)告。
視來(lái)源而定,會(huì)得到“無(wú)法裝入模塊”或者“未被發(fā)現(xiàn)的異常被漏過(guò)”的信息。筆者編寫了一個(gè)小小的DebugUtility類,它提供了易于定制的默認(rèn)客戶端錯(cuò)誤處理機(jī)制(見代碼片段1)。
在服務(wù)器端,可以使用java.util.logging API或者log4j的廣泛功能,具體取決于個(gè)人偏好或者項(xiàng)目的約束條件。但要是沒有為GWT的 com.google.gwt.user.server.rpc.RemoteServiceServlet類打補(bǔ)丁,對(duì)于未被發(fā)現(xiàn)、未被檢查的異常,只 會(huì)在堆棧跟蹤里面得到提示,指向生成該錯(cuò)誤的服務(wù)器端類。對(duì)于catch()代碼塊里面發(fā)現(xiàn)及報(bào)告的被檢查的異常,一切都正常。
忠告之三: 當(dāng)心GWT Shell的“刷新”按鈕陷阱
在宿主模式下啟動(dòng)應(yīng)用時(shí),會(huì)在瀏覽器任務(wù)欄上看到“刷新”按鈕。要是摁了這個(gè)按鈕,GWT就會(huì)把修改過(guò)的Java 客戶端源代碼重新編譯成Java字節(jié)碼(作為.gwt.-cache/bytecode目錄中的.tmp文件),然后重新裝入模塊。可以使用這個(gè)按鈕來(lái)縮 短編輯→編譯→調(diào)試周期,但在使用這項(xiàng)特性時(shí)要牢記幾個(gè)方面:
● 只有修改過(guò)的源代碼才重新編譯,也就是說(shuō),不會(huì)為依賴修改過(guò)代碼的文件生成新的字節(jié)碼。所以,如果改變了全局常量的值,假設(shè)public final int字段的值,不會(huì)立即在相關(guān)文件看到這個(gè)變化。
● 只有修改過(guò)的源代碼才由GWT重新編譯。這意味著,即便Eclipse IDE里面的“Project clean”也幫不上忙; 要影響到所有的相關(guān)源代碼,譬如通過(guò)添加新的空行。
因?yàn)檫@個(gè)過(guò)程相當(dāng)笨拙,筆者的忠告是在修改全局常量時(shí)遵循以下四個(gè)步驟:
1.在相應(yīng)的源文件里面改變public final constant值;
2.重新編譯改變后的源代碼;
3.移除整個(gè)< ModuleName>-client/.get-cache/bytecode目錄,從而刪除GWT緩存內(nèi)容;
4、使用Eclipse里面的“Run as”,重新開始啟動(dòng)應(yīng)用,從而創(chuàng)建帶重新編譯后字符碼的新GWT緩存內(nèi)容,這種情況下,最好忽視“刷新”按鈕,不過(guò)在有些情況下,刪除整個(gè)< ModuleName>-client/.get-cache/bytecode目錄后可以使用“刷新”按鈕。
在修改服務(wù)器端代碼時(shí),GWT字節(jié)碼緩存內(nèi)容不受影響。不過(guò),嵌入的Tomcat實(shí)例會(huì)緩存它,因而在使用“刷新”按鈕后,只有重新開始啟動(dòng)應(yīng)用后最初改變的代碼才會(huì)得到認(rèn)可。所以為了安全起見,改變服務(wù)器端代碼后,最好還是重新開始啟動(dòng)應(yīng)用。
忠告之四: 在宿主模式下讀取Servlet Init參數(shù)
在處理數(shù)據(jù)庫(kù)系統(tǒng)時(shí),一般不希望服務(wù)器小程序源代碼中有硬編碼的數(shù)據(jù)庫(kù)連接參數(shù)。通常會(huì)從屬性文件讀取這些參數(shù); 或者更好的是,把它們作為init參數(shù)提供給服務(wù)器小程序(作為應(yīng)用的Web.xml文件的一部分)。如果在Web模式下運(yùn)行應(yīng)用那沒有什么,但在宿主模 式下會(huì)出問(wèn)題,這是由于GWT宿主模式下的服務(wù)器小程序處理存在限制。
好消息是,只要修改由嵌入式Tomcat實(shí)例使用的Web.xml文件,就可以解決這個(gè)問(wèn)題。為此,修 改< ModuleName>-client/tomcat/webapps/ROOT/WEB-INF目錄中的Web.xml文件(或者必要時(shí)創(chuàng)建一 個(gè)): 除了嵌入式Tomcat的GWTShellServlet映射外,添加帶有init參數(shù)的上下文部分。因?yàn)樯舷挛男畔⑹?#8220;全局性的”,而不是針對(duì)特定的服 務(wù)器小程序,在這里只有一部分的init參數(shù)信息,或者使用特殊的命名方案,把參數(shù)與不同的服務(wù)器小程序聯(lián)系起來(lái)。如果使用這個(gè)新的web.xml文件, 可以刪除src/web/WEB-INF文件夾中的那個(gè)舊文件。
在服務(wù)器小程序代碼中,訪問(wèn)init參數(shù)的方式與Web模式下讀取它們的方式一樣,譬如final String host = getInitParameter("host")。筆者實(shí)現(xiàn)這一點(diǎn)的辦法就是修改GWT的RemoteServiceServlet,方法跟第二個(gè)忠告 里面的如出一轍。現(xiàn)在,只要覆蓋GenericServlet的getInitParameter()方法,以便使用 getServletContext(),而不是 getServletConfig()。
另一個(gè)忠告是,如果在宿主模式下和Web模式下測(cè)試不同的服務(wù)器代碼,略過(guò)Gant腳本中的GWT編譯部分,從“temp”位置拷貝編譯前的JavaScript代碼,則可以節(jié)省時(shí)間。這適用于客戶端代碼復(fù)雜、編譯時(shí)間超過(guò)10分鐘的情形。
忠告之五: 在瀏覽器里面顯示PDF文件
大多數(shù)實(shí)際的Web應(yīng)用提供了生成及閱讀PDF文件的方法。本文假設(shè)這個(gè)PDF文件由服務(wù)器小程序生成,譬如通過(guò) JasperReport。以后只要點(diǎn)擊某個(gè)超文本鏈接,就可以在瀏覽器里面閱讀生成的文件。如果想在宿主模式下和Web模式下測(cè)試這項(xiàng)特性,建議采取以 下步驟:
1.設(shè)計(jì)一個(gè)RPC接口,接受告訴服務(wù)器是在宿主模式下運(yùn)行還是在Web模式下運(yùn)行的布爾參數(shù)。接口方法會(huì)返回的字符串應(yīng)當(dāng)帶有服務(wù)器小程序生成的PDF文件的名稱(即文件名的最后一部分)。
2.根據(jù)代碼片段4顯示的代碼,實(shí)現(xiàn)服務(wù)器小程序代碼,這取決于布爾參數(shù)“isScript”。
3.在客戶端: 在窗口組件代碼里面,使用GWT.isScript()參數(shù)調(diào)用createXyzPDF()方法,從而生成包含服務(wù)器小程序結(jié)果字符串的外部超文本鏈接。
代碼片段4顯示了接口方法名為createSummaryPDF()的示例。從服務(wù)器小程序返回的字符串是“summary.pdf”。
這當(dāng)然不是處理這種情況的惟一辦法,但目前適用于我們這個(gè)示例。請(qǐng)注意: 在宿主模式下啟動(dòng)應(yīng)用之前,必須在< ModuleName>-client project's src/…/public文件夾中至少創(chuàng)建一個(gè)虛假的“summary.pdf”文件(文件名從服務(wù)器小程序返回)。不然,在瀏覽器中點(diǎn)擊了超文本鏈接 后,GWT試圖讀取PDF文件時(shí),會(huì)出現(xiàn)“HTTP 404-找不到網(wǎng)頁(yè)”的信息。
忠告之六:力求獲得無(wú)狀態(tài)服務(wù)器
設(shè)計(jì)客戶機(jī)/服務(wù)器Web應(yīng)用時(shí)要考慮的一個(gè)關(guān)鍵問(wèn)題就是: 如何處理會(huì)話和狀態(tài)管理?在Web 1.0時(shí)代,答案很顯然: 會(huì)話和狀態(tài)管理是一個(gè)服務(wù)器問(wèn)題。但若使用GWT,就有另一個(gè)選擇。服務(wù)器再也不是只提供HTML內(nèi)容的“web”服務(wù)。使用GWT RPC,服務(wù)器現(xiàn)在可以支持只提供結(jié)構(gòu)化數(shù)據(jù)的服務(wù)———在本文示例中,服務(wù)由服務(wù)器小程序?qū)崿F(xiàn)。
那么,GWT對(duì)會(huì)話和狀態(tài)管理有何影響呢?GWT的技術(shù)領(lǐng)導(dǎo)Bruce Johnson在去年的JAOO大會(huì)上指出,若使用GWT,會(huì)話管理現(xiàn)在應(yīng)當(dāng)是一個(gè)客戶端問(wèn)題。附圖顯示的幻燈片評(píng)述了種種變化。
在本文的JUnit2MR GWT應(yīng)用中,筆者一開始使用傳統(tǒng)方法來(lái)處理服務(wù)器小程序中的會(huì)話狀態(tài)。但這是相當(dāng)笨拙的任務(wù),于是尋找另一種選擇。因此,看了Bruce的幻燈片后,決 定重新設(shè)計(jì)整個(gè)應(yīng)用。但這一步需要改變所有RPC接口、緩存策略; 最重要的是,還要改變所有的服務(wù)器小程序。因此筆者的建議是: 及早考慮在何處實(shí)施會(huì)話和狀態(tài)管理,不妨試試Bruce Johnson的訣竅。最終會(huì)收到成效。
由于這個(gè)決定,客戶端對(duì)象之間有了更多的聯(lián)系。于是筆者使用了有名的GoF中介者模式(mediator pattern)。不過(guò),在客戶端有一些JDK 1.4和GWT運(yùn)行庫(kù)的限制。因此,重新實(shí)現(xiàn)了PropertyChangeEvent類和中介者支持,來(lái)處理監(jiān)聽程序注冊(cè)和消息廣播。
忠告之七: 使用Selenium實(shí)現(xiàn)GWT Web測(cè)試的自動(dòng)化
Selenium是一種開源工具,它能夠輕松測(cè)試包含豐富、互動(dòng)的客戶端內(nèi)容的Web應(yīng)用。 所以,它非常適用于測(cè)試像用GWT創(chuàng)建的應(yīng)用那樣的Ajax應(yīng)用。
當(dāng)然,GWT里面仍有JUnit和JUnit支持功能,特別是針對(duì)系統(tǒng)的異步部分。這里著重介紹 Selenium,因?yàn)樗子谑褂茫ㄖ辽偎腎DE是這樣)、功能強(qiáng)大。最后但并非最不重要的一點(diǎn)是,它與JUnit有許多共同之處。可以使用 Selenium IDE來(lái)記錄GUI用例,然后使用其“Play”特性來(lái)運(yùn)行記錄下來(lái)的操作。每個(gè)操作之后跟著類似JUnit的“assert”命令,負(fù)責(zé)確認(rèn)頁(yè)面上的某 些文本。該IDE是Firefox的擴(kuò)展插件,但務(wù)必要使用最新版本的Selenium: Selenium IDE 0 .8 .7,因?yàn)樗?#8220;waitFor…”命令的重大修正版。說(shuō)到測(cè)試Ajax應(yīng)用,這些命令以及“pause”命令非常重要。
忠告之八: 使用Groovy Gant腳本部署應(yīng)用
在GWT宿主模式下試運(yùn)行應(yīng)用,這確實(shí)很好,但把應(yīng)用部署到應(yīng)用服務(wù)器上或者類似Tomcat的服務(wù)器小程序容器 上,GWT的真實(shí)功能才會(huì)體現(xiàn)出來(lái)。在這一步,需要?jiǎng)?chuàng)建一個(gè)war文件,它會(huì)自動(dòng)拷貝到Tomcat“webapps”目錄。當(dāng)然,可以使用Ant和 ant-contrib進(jìn)行所有必要的準(zhǔn)備、編譯、拷貝及其他任務(wù)。但由于Ant腳本變得更復(fù)雜后, ant-contrib控制結(jié)構(gòu)和屬性regex處理有一點(diǎn)笨拙。于是可以使用集Groovy和Ant兩者之所長(zhǎng)的Gant。安裝Groovy和Gant 用不了10分鐘,然后,使用來(lái)自“build.properties”文件的普通屬性,即可定制“build.gant”腳本。(小黑編譯)
(計(jì)算機(jī)世界報(bào) 2007年6月25日 第24期 B22、B23)