原文出處:
XML.comhttp://www.xml.com/pub/a/2006/07/12/google-web-toolkit-ajax-java-ant-xml.html翻譯:劉嘉晗
http://www.tkk7.com/johnlauGoogle Web Toolkit
By Bruce Perry
2006年7月12日
如果你是一名Java軟件或Ajax開發(fā)者,可能Google Web Toolkit(GWT)已經(jīng)引起了你的注意。
2006年5月,Google發(fā)布了這一免費的工具箱,遵循Apache風格的許可證。GWT被設計用于以Java語言編寫Ajax應用。Google已經(jīng)提供了Windows和Linux下的beta版本,并承諾以后增加Mac OS X版本。
本文描述了在Max OS X上一個簡單的Ajax應用的開發(fā),使用GWT和常用的Java工具,如Apache Ant,Tomcat 5.0 Servlet容器,以及IntelliJ IDEA集成開發(fā)環(huán)境(后者是一款商業(yè)IDE)。本文假設讀者具有Java和Ant的基本知識。
結合使用Ant與GWT
我下載了GWT Linux beta版本,用Java編寫了我的應用程序,然后用一個Ant構建文件編譯和在一個Tomcat 5.0實例上部署了此程序。Ant文件執(zhí)行GWT Java-to-JavaScript編譯器。此“編譯器”是一個命令行腳本,它執(zhí)行一個GWT Java類,而后者為應用程序輸出JavaScript腳本。
使用GWT beta包括兩種開發(fā)模式:宿主(host)模式和web模式。
宿主模式是一個開發(fā)中的中間步驟,它使用內嵌的GWT瀏覽器;在這種模式下,已編譯代碼繼續(xù)在Java虛擬機(JVM)中運行。然而,宿主模式對于我們這些借用Linux版本的Mac OS X用戶還不可用。Mac OS X下的宿主模式要等到Max OS X版本發(fā)布了。
一種不同的web開發(fā)方式
本文探討了GWT開發(fā)者在為遠程過程調用(RPC)創(chuàng)建服務時可能面對的一些典型的web開發(fā)相關的任務。RPC是用于使用面向服務架構(SOA)的應用的軟件模型的一部分。這些開發(fā)任務包括:
- 使用構建文件自動化開發(fā)和開發(fā)步驟(構建過程運行GWT編譯器,然后將編譯器的輸出和服務器端Java類文件部署到一個servlet容器,如Tomcat,Jetty或Resin)。
- 使用Firefox的DOM Inspector查看GWT應用程序生成的HTML。
- 重新設計頁面控件而不需改動底層HTML(因為你在使用GWT的Java API)。
- 確定HTML已合法地標記,例如,基于你的組織所要求的特定格式的XHTML文檔。
為您服務
首先,我會簡要描述該應用程序創(chuàng)建的服務。它被設計用來展示GWT采用的模式。
該應用程序在瀏覽器中顯示一個表單,要求用戶輸入名字,年齡,以及國家。當用戶通過點擊按鈕提交表單時,程序在一個文本區(qū)內顯示一條服務器回應而不需刷新頁面。圖1是程序在Safari瀏覽器中的顯示。

圖1: GWT生成的一個簡單視圖
例如,當用戶在一個文本框為空時點擊OK,Submit按鈕時,結果如圖2所示。

圖2: 應用程序以紅色顯示一條錯誤信息
巧妙的服務機制
在Ajax應用中使用RPC消除了顯式地處理XMLHttpRequest和相關聯(lián)的服務器返回值的需要,因為GWT對象會為你處理這些通訊工作。
程序定義的每個服務需要兩個Java接口和一個Java類。為了編譯這些類,需要確定gwt-user.jar庫在classpath中(Ant文件中增加一個條目就可以解決)。以下代碼示范了定義我們的服務的Java接口。
package?com.parkerriver.gwt.testapp.client;
import?com.google.gwt.user.client.rpc.RemoteService;
public?interface?ShowRespService?extends?RemoteService{
????String?displayResponse(String?req);
}
服務接口必須擴展GWT接口RemoteService。它只定義了一個方法 displayResponse()。
你還需要定義一個接口,客戶端或最終被下載的JavaScript代碼將會用它調用服務方法。GWT使用了一種回調設計模式,當我給出客戶端代碼的時候會再來描述它(請看MyForm.java)。
package?com.parkerriver.gwt.testapp.client;
import?com.google.gwt.user.client.rpc.AsyncCallback;
public?interface?ShowRespServiceAsync?{
????public?void?displayResponse(String?s,
????????????????????????????????AsyncCallback?callback);
}
要使一個服務在GWT中可用必須遵守命名約定;在服務接口名(ShowRespService)后增加Async后綴。AsyncCallback對象是GWT API的一部分,它的目的是為客戶端處理服務響應。不管怎樣,等你看了這段代碼應用的地方后會對這一行為有更清晰的了解。這些對象定義都位于用于生成客戶端JavaScript的Java代碼中。
一個改頭換面的servlet
最后,你需要定義一個Java類來實現(xiàn)遠程服務接口。這個類將會存在于你的Ajax應用程序的服務器端。
package?com.parkerriver.gwt.testapp.server;
import?com.parkerriver.gwt.testapp.client.ShowRespService;
import?com.google.gwt.user.server.rpc.RemoteServiceServlet;
import?java.util.Date;
public?class?ShowRespServiceImpl?extends?RemoteServiceServlet
????????implements?ShowRespService??{
????public?String?displayResponse(String?req)?{
????????if(req.length()?<?1)?{
????????????throw?new?IllegalArgumentException(
????????????????????"Blank?submissions?from?the?client?are?invalid.");
????????}
????????StringBuffer?buf?=?new?StringBuffer("Your?submission:?");
????????Date?date?=?new?Date();
????????String?serverInfo?=?this.getServletContext().getServerInfo();
????????buf.append(req);
????????buf.append("\n");
????????buf.append("Server?response:?");
????????buf.append(date.toString());
????????buf.append("\n");
????????buf.append(serverInfo);
????????return?buf.toString();
????}
}
該類必須擴展RemoteServiceServlet,這是一個GWT API對象,它本身擴展了javax.servlet.http.HttpServlet。也就是說,該類和它實現(xiàn)的接口需要被部署到你的servlet容器。
步驟
現(xiàn)在我們已經(jīng)定義了服務,讓我們退回幾分鐘來看一看程序的目錄結構。Google Web Toolkit包括了一個名為applicationCreator的命令行腳本,它將會為你生成項目目錄結構。解壓縮GWT下載包后,你可以在頂層目錄中找到applicationCreator。我使用下面的命令行來開始:
applicationCreator?-out?/Users/bruceperry/1gwt/secondapp/?com.parkerriver.gwt.testapp.client.MyForm
圖3顯示了目錄結構

圖3: 一個GWT和IntelliJ項目目錄
applicationCreator生成./src目錄和MyForm-compile以及MyForm-shell腳本。我的Ant文件執(zhí)行MyForm-compile;另一個腳本運行宿主模式。./src目錄包括與最初的包命名相符的嵌套目錄結構,如圖4所示。

圖4:一個GWT應用程序的包和模塊
文件MyForm.gwt.xml是生成的配置文件,GWT稱之為“模塊(module)”。它為你的程序定義了表示“入口點(entry point)”的Java類,類似于包含main()方法的Java類。
<module>
????<!--Inherit?the?core?Web?Toolkit?stuff.??????????????????-->
????<inherits?name="com.google.gwt.user.User"/>
????<!--Specify?the?app?entry?point?class.???????????????????-->
????<entry-point?class="com.parkerriver.gwt.testapp.client.MyForm"/>
</module>
其它的文件或目錄是IntelliJ Web application project的產(chǎn)物,包括./classes,./WEB-INF,和./gwtproj.ipr,所以不需要特別留意它們。
另外,./www目錄之前并不會出現(xiàn)(除非你自己創(chuàng)建它),直到運行GWT編譯器生成程序代碼。我的項目使用Ant文件getproj.xml,而項目屬性在gwtproj.properties中定義。在我展示Ant構建文件之前,我們先看一下代表程序入口點的MyForm.java類。
入口點
MyForm.jaca類實現(xiàn)了GWT API接口EntryPoint。所以,該類必須實現(xiàn)onModuleLoad()方法,當瀏覽器載入你的Ajax程序時,瀏覽器的JavaScript引擎將會調用它。
換句話說,GWT編譯器將這個類編譯為JavaScript代碼。MyForm.java類為瀏覽器視圖建立表單控件。該類同時決定了用戶點擊OK,Submit按鈕時的響應。代碼中的注釋詳細描述了到底發(fā)生了什么,所以我在正文中不再贅述。
package?com.parkerriver.gwt.testapp.client;
import?com.google.gwt.core.client.EntryPoint;
import?com.google.gwt.core.client.GWT;
import?com.google.gwt.user.client.DOM;
import?com.google.gwt.user.client.Window;
import?com.google.gwt.user.client.Element;
import?com.google.gwt.user.client.rpc.ServiceDefTarget;
import?com.google.gwt.user.client.rpc.AsyncCallback;
import?com.google.gwt.user.client.ui.*;
import?java.util.Iterator;
public?class?MyForm?implements?EntryPoint?{
????//提供狀態(tài)信息的HTML div元素的id
??? private?String?statusId?=?"status";
????//一個Grid對象;實際上,是一個HTML table
????private?Grid?grid?=?new?Grid(5,?2);
????//其它用戶界面對象
????private?Label??nmLab?=?new?Label();
????private?Label??ageLab?=?new?Label();
????private?Label??homeLab?=?new?Label();
????private?TextBox?nmTxt?=?new?TextBox();
????private?TextBox?ageTxt?=?new?TextBox();
????private?TextBox?homeTxt?=?new?TextBox();
????private?Button?okBut?=?new?Button();
????private?TextArea?tarea?=?new?TextArea();
????/* 當瀏覽器載入應用程序時本方法被調用。
??????? 本方法設置3個標簽和文本框,以及一個
??????? 按鈕和用來顯示服務器相應的文本區(qū)*/
?????public?void?onModuleLoad()?{
????????//設置標簽和文本域
????????nmLab.setText("Full?Name:");
????????nmTxt.setMaxLength(25);
????????ageLab.setText("Age:");
????????ageTxt.setVisibleLength(3);
????????ageTxt.setMaxLength(3);
????????homeLab.setText("Home?country:");
????????homeTxt.setMaxLength(25);
????????//將這些控件放入Grid中
????????grid.setWidget(0,0,nmLab);
????????grid.setWidget(0,1,nmTxt);
????????grid.setWidget(1,0,ageLab);
????????grid.setWidget(1,1,ageTxt);
????????grid.setWidget(2,0,homeLab);
????????grid.setWidget(2,1,homeTxt);
????????//設置按鈕和文本區(qū)
????????tarea.setCharacterWidth(40);
????????tarea.setVisibleLines(25);
????????okBut.setText("OK,?Submit");
????????//通過增加一個listener對象為按鈕設置行為
????????//詳細地講,一個帶有onClick()事件處理器的
????????//ClickListener對象。
????????okBut.addClickListener(new?ClickListener()?{
????????????public?void?onClick(Widget?sender)?{
????????????????//提示用戶遠程過程調用的狀態(tài);
????????????????//見下面的方法
????????????????showRpcStatus(true);
????????????????//為服務器端服務創(chuàng)建一個客戶端存根的實例
??????????????? ShowRespServiceAsync?respService?=
????????????????????????(ShowRespServiceAsync)?GWT
????????????????????????????????.create(ShowRespService.class);
????????????????ServiceDefTarget?endpoint?=?(ServiceDefTarget)?respService;
????????????????//我們的服務的實現(xiàn)是一個RemoteServiceServlet的實例,
????????????????//所以提供到該servlet的服務器路徑
????????????????//該路徑為web.xml中設置的值
????????????????endpoint.setServiceEntryPoint("/parkerriver/s/showresp");
????????????????//該接口處理服務器響應。
????????????????//它會在一個文本區(qū)中顯示服務器的響應。
????????????????//如果回復消息表示一個錯誤
????????????????//則將用紅色字體顯示
????????????????AsyncCallback?callback?=?new?AsyncCallback()?{
????????????????????public?void?onSuccess(Object?result)?{
????????????????????????//如有,則移除與錯誤消息外觀相關的‘warning’CSS樣式
????????????????????????//
????????????????????????if(tarea.getStyleName().
????????????????????????????????equalsIgnoreCase("warning")){
????????????????????????????tarea.removeStyleName("warning");
????????????????????????}
????????????????????????//文本區(qū)顯示服務器的返回值
????????????????????????tarea.setText((String)result);
????????????????????}
????????????????????public?void?onFailure(Throwable?caught)?{
????????????????????????//文本區(qū)顯示任何異常消息
????????????????????????tarea.setStyleName("warning");
????????????????????????tarea.setText(
????????????????????????????"Server?request?raised?an?error;?Java?exception?:?"+
????????????????????????????caught?==?null???"An?unknown?exception"?:
????????????????????????????????????????caught.getMessage());
????????????????????}
????????????????};
????????????????//調用服務方法。
????????????????//首先驗證表單值。
????????????????try{
????????????????????respService.displayResponse(
????????????????????????????getPanelTextContent(grid,true),
????????????????????????????callback);
????????????????}?catch?(Exception?e)?{
????????????????????tarea.setStyleName("warning");
????????????????????tarea.setText("Server?request?raised?an?error:?"+
????????????????????????????e.getMessage());
????????????????}??finally?{
????????????????????//當我們完成RPC調用時
????????????????????//移除狀態(tài)信息
????????????????????showRpcStatus(false);
????????????????}
????????????}
????????});
????????//現(xiàn)在將這些控件加到Grid上
????????grid.setWidget(3,0,okBut);
????????grid.setWidget(3,1,tarea);
????????//為OK按鈕的單元設定垂直對齊屬性
????????grid.getCellFormatter().setVerticalAlignment(3,0,
????????????????HasVerticalAlignment.ALIGN_TOP);
????????//為文本框設置垂直對齊屬性,
????????//以使它們合適地排列
????????grid.getCellFormatter().setVerticalAlignment(0,1,
????????????????HasVerticalAlignment.ALIGN_BOTTOM);
????????grid.getCellFormatter().setVerticalAlignment(1,1,
????????????????HasVerticalAlignment.ALIGN_BOTTOM);
????????grid.getCellFormatter().setVerticalAlignment(2,1,
????????????????HasVerticalAlignment.ALIGN_BOTTOM);
????????//將grid,實際上是一個HTML table,加到
????????//瀏覽器HTML中id值為"gridholder"的div元素中。
????????RootPanel.get("gridholder").add(grid);
????}
????/*簡單地測試是否有為空的域,然后以單一字符串返回
??? ?? 提交的值。
??? ?? HasWidgets是Grid和其它panel類型的對象實現(xiàn)的一個接口。
??? ?? 因此,我們可以將grid傳入該方法;遍歷它包含的文本框,
??? ?? 并且驗證文本框的內容。
????*/
????private?String?getPanelTextContent(HasWidgets?panelType,
???????????????????????????????????????boolean?validateContent)?{
????????StringBuffer?buf?=?new?StringBuffer("");
????????String?tmp?=?null;
????????if(panelType?!=?null)?{
???????????//為了簡介,省略
????????}
????????//返回以空格分隔的文本框的內容
??????? return?buf.toString();
????}
????
????/* 過于簡化的驗證!?*/
????private?boolean?validateText(String?_content){
????????return?_content.length()?>?0;
????}
????private?int?getTextboxCount(HasWidgets?pType){
????????//未顯示: 返回panel中TextBox控件的數(shù)量
???????
????}
????/* 如果響應很長時間才到達
???? 則顯示給用戶一個狀態(tài)信息。*/
????private?void?showRpcStatus(boolean?_on){
????????//利用GWT DOM API進行JavaScript DOM編程
??????
????????Element?el?=?DOM.getElementById(statusId);
????????if(el?!=?null)??{
????????????if(_on)?{
????????????????DOM.setStyleAttribute(el,"font-size","1.2em");
????????????????DOM.setStyleAttribute(el,"color","green");
????????????????DOM.setInnerHTML(el,?"Fetching?server?info
");
????????????}??else{
????????????????DOM.setInnerHTML(el,?"");
????????????}
????????}
????}
} 代碼中大多部分處理GWT API。值得一提的是如果你需要實現(xiàn)JavaScript DOM編程,如showRpcStatus()方法中那樣,你可以通過使用com.google.gwt.user.client.DOM類實現(xiàn)這個任務。
構建文件
以下是Ant構建文件的重點;它:
- 編譯Java文件,結果寫入項目目錄中的./classes目錄。
- 執(zhí)行GWT編譯腳本(本例中名為MyForm-compile)。
- 將./www目錄中生成的結果代碼移到一個已部署到Tomcat上的更大的web應用。
- 復制編譯后的Java servlet和相關接口(ShowRespService)到同一個web應用。
負責編譯Java類和執(zhí)行到JavaScript的轉換的兩個Ant任務定義為如果產(chǎn)生任何錯誤,則使整個構建失敗。
Ant XML
這里是gwtproj.properties文件包含的部分內容:
web.deploy.location=/users/bruceperry/parkerriver/gwt
web.classes.location=/users/bruceperry/parkerriver/WEB-INF/classes
下面的XML表示了上述Ant文件的主要內容;完整文件的鏈接在本文的資源一節(jié)中。
<?xml?version="1.0"?encoding="UTF-8"?>
<project?name="gwtproj"?default="all">
????<property?file="gwtproj.properties"/>
????
?????<!--?The?top-level?directory?for?the?project?and
????where?the?ant?file?resides?-->
????<dirname?property="module.gwtproj.basedir"?file="${ant.file}"/>
????
?????<!--?The?./classes?directory?inside?the?top-level?directory?-->
?????<property?name="gwtproj.output.dir"?value=
????????????"${module.gwtproj.basedir}/classes"/>
????????????
????<!--?This?target?calls?MyForm-compile?to?create
????all?the?content?in?the?./www?directory?-->
????<target?name="gwt-compile"?depends=
????????????"compile.production.classes"
????????????description="use?gwt's?compiler">
????????<delete>
????????????<fileset?dir="${web.deploy.location}"?includes="**/*"/>
????????</delete>
????????<exec?executable=
????????????????"${module.gwtproj.basedir}/MyForm-compile"
??????????????failonerror="true"/>
????????<copy?todir="${web.deploy.location}">
????????????<fileset?dir=
????????????????????"${module.gwtproj.basedir}/www">
????????????</fileset>
????????</copy>
????</target>
????<target?name="compile.production.classes"?description=
????????????"Compile?the?gwtproj?production?classes">
????????<mkdir?dir="${gwtproj.output.dir}"/>
????????<javac?destdir="${gwtproj.output.dir}"?debug=
????????????????"on"?failonerror="true"?nowarn=
????????????????"off"?memoryMaximumSize="128m"?fork=
????????????????"true"?executable="${module.jdk.home.gwtproj}/bin/javac">
????????????<classpath?refid="gwtproj.module.classpath"/>
????????????<src?refid="gwtproj.module.sourcepath"/>
????????</javac>
????</target>
????
??<!--?copy?the?Java?servlet?classes?to?the?web?application?-->
????<target?name="deploy.classes"??depends="gwt-compile"
????????????description="copy?classes?to?web?directory">
????????<copy?todir="${web.classes.location}">
????????????<fileset?dir="${gwtproj.output.dir}">
????????????</fileset>
????????</copy>
????</target>
????
????<target?name="all"?depends="deploy.classes"
????????????description="build?all"/>
</project>
你可以在IDE中運行該Ant文件(比如在IntelliJ中)或者在包含構建文件的目錄中運行下面的命令行:
ant?-buildfile?gwtproj.xml
大多數(shù)情況下,在更改應用程序并運行Ant后,你可以通過在瀏覽器中刷新頁面看到所作的改變。
最后設置
在最后的設置上,你可能需要了解的是添加gwt-user.jar庫到你的web應用的/WEB-INF/lib目錄。
我創(chuàng)建了我自己的JAR文件,去掉了其中的javax包,命名為gwt-user-deploy.jar,并添加到/WEB-INF/lib。這是因為Tomcat將不會載入web應用程序中包含servlet API類的庫。
web開發(fā)者的吹毛求疵
applicationCreator同時創(chuàng)建了你的Ajax應用程序的HTML前端,本例中名為MyForm.html。
如果你的應用程序的HTML需要遵從如XHTML transitional或Strict時怎么辦呢?對于XHTML transitional的情況,我首先在MyForm.html頂端增加了所需的DOCTYPE,以及html標簽的相關屬性:
<!DOCTYPE?html?PUBLIC?"-//W3C//DTD?XHTML?1.0?Transitional//EN"
????????"http://www.w3.org/TR/2000/REC-xhtml1-20000126/DTD/xhtml1-transitional.dtd">
<html?xmlns="http://www.w3.org/1999/xhtml"?xml:lang="en"?lang="en">
然后我上傳MyForm.html到http://validator.w3.org/的World Wide Web協(xié)會的HTML驗證器。
運行驗證器后,我對HTML做了一些簡單的修改,如合適地關閉元標簽以及增加type="text/javascript"到script標簽。
Strict:嘖嘖
然而,如果你要遵從XHTML Strict標準,則可能需要做一些更復雜的更改。例如,W3C的驗證器將iframe標簽標為“undefined element”,iframe是GWT的歷史支持所需要的(提供如瀏覽器的后退按鈕同樣的功能)。XHTML已經(jīng)移除了iframe元素。
這對你可能不是一個問題(可能會同其它明顯問題一起在GWT的未來版本中解決);但是,你可以自己實現(xiàn)替代策略,如擴展GWT的類和創(chuàng)建你自己的兼容的控件。
使控件各就各位
視覺外觀設計是web開發(fā)中總是出現(xiàn)的問題。項目設計者希望頁面看起來和他們在Adobe Illustrator中創(chuàng)建的一摸一樣,是嗎?
盡管在復雜的Ajax項目中你可能無法實現(xiàn)這一使程序養(yǎng)眼的理想,至少你可以使用Firefox的DOM Inspector來檢視你的Java類最終生成的HTML。那么從這里開始。
打開Firefox的Tools=>DOM Inspector菜單項(如圖5)

圖5: 用DOM Inspector查看HTML
可見Java代碼中的com.google.gwt.user.client.ui.Grid對象實現(xiàn)為一個HTML table標簽。table中包含OK,Submit按鈕的TD標簽與一個樣式屬性“verticle-align:top”關聯(lián)。
這使按鈕與文本區(qū)頂端對齊。下面是MyForm.java類中設置對齊方式的代碼:
//set?the?vertical?alignment?for?the?OK?button's?cell
????????grid.getCellFormatter().setVerticalAlignment(3,0,
????????????????HasVerticalAlignment.ALIGN_TOP);
如果代碼中沒有此調用,按鈕就會很業(yè)余地掛在文本區(qū)的中間區(qū)域。
現(xiàn)在要做的只是使按鈕與它上面的文本標簽左對齊。
資源
?? ?Google Web Toolkit:
http://code.google.com/webtoolkit/??? 本文代碼:
gwtarticle_jul06.zip
XML.com Copyright?? 1998-2006 O'Reilly Media, Inc.