最近发布的Google Web Toolkit (GWT)是一组全面的API和工具,它支持用户几乎完全使用Java代码来创建动态Web应用程序。Philip McCarthy回到了他广受欢迎的面向Java开发人员的Ajax系列,向您展示GWT能做什么,并帮助您确定它是否适合您。
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源代码:
清单1. 最简单的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);
}
}
这个代码看起来非常像使用Swing、AWT或SWT编写的GUI代码。不出所料,清单1创建了一个按钮,在单击此按钮时会显示消息“Hello World!”。该按钮被添加到RootPanel,这是一个环绕HTML页面主体的GWT包装对象。图1显示了应用程序在GWT Shell中运行时的情况。GWT Shell是一个包含在GWT SDK中的调试宿主环境(debugging hosting environment),与一个简单的浏览器组合在一起。
图1. 运行最简单的GWT示例
构建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浏览器时,将调用这个方法。
清单2. Weather Reporter应用程序的布局代码
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中呈现的布局:
图2. 基本GUI布局
用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类名。
清单3. 将CSS样式应用到Weather Reporter应用程序的输入面板
/* 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显示了应用程序被替换成这些样式之后的情况:
图3. 应用了这些样式之后的输入面板
添加客户端行为
现在应用程序的基本布局和样式已经就绪,我将开始实现一些客户端行为。可以用熟悉的侦听器模式在GWT中执行事件处理。GWT为鼠标事件、键盘事件、修改事件等提供了Listener接口,还提供了几个适配器和助手类,以获得更多方便。
一般情况下使用Swing程序员熟悉的内部类形式来添加事件侦听器。但是,所有GWT Listener方法的第一个参数都是事件的发送者,通常是用户刚刚与之交互的小部件。这意味着可以把同一个Listener实例附加到所需的多个小部件上;可以用sender参数确定是哪个小部件触发了事件。
清单4显示了Weather Reporter应用程序中实现的两个事件侦听器。click句柄被添加到了Submit按钮上,keyhandler被添加到了TextBox上。不管是单击Submit按钮,还是在TextBox拥有焦点时按下回车键,都会导致相关的句柄调用私有的validateAndSubmit()方法。在添加到清单4的代码中之后,txBox和ucRadio已经成为Weather类的实例变量,所以可以从验证方法访问它们。
清单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()方法的实现。该实现非常简单,由封装验证逻辑的ZipCodeValidator类完成。如果用户没有输入正确的5位数字的ZIP代码,那么validateAndSubmit()将在警告框中显示错误消息,如果这种情况出现在GWT中,则会调用Window.alert()。如果ZIP代码正确,那么它将与用户对摄氏或华氏温度单位的选择一起被传递给fetchWeatherHtml()方法,这个方法稍后再介绍。
清单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进行客户端调试
在这里我要岔开一会,提一下GWT Shell,它拥有允许在Java IDE中调试客户端代码的JVM挂钩。您可以与Web UI进行交互,分步调试表示客户端执行的相应JavaScript代码的Java代码。这是一项很重要的功能,因为在客户端上调试所生成的JavaScript基本上是不可能的。
可以很容易地配置一个Eclipse调试任务,从而通过com.google.gwt.dev.GWTShell类启动GWT Shell。图4显示了按下Submit按钮后,在validateAndSubmit()方法的断点处暂停的Eclipse:
图4. 调试客户端GWT代码的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的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描述。
清单6. 远程WeatherService接口和部分实现
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的异步接口:
清单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()的实现,这个实现我在前面提到过(请参阅添加客户端行为):
清单8. 使用RPC调用远程服务
// 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);
}
});
}
对服务的getWeatherHtml()的实际调用实现起来非常简单:使用一个匿名回调句柄类将服务器的响应传递给显示响应的方法即可。
图5显示了应用程序的运行情况,显示了从Yahoo!天气API检索的天气报告:
图5. Weather Reporter应用程序显示了从Yahoo!得到的报告
服务器端验证的需要
用GWT合并客户端和服务器端代码存在内在危险。因为您使用Java语言来编写所有代码,所以GWT的抽象隐藏了客户机/服务器之间的分离,很容易让人误认为可以相信运行时的客户端代码。这是错误的。Web浏览器上运行的任何代码都可能被恶意用户篡改或者完全绕开。GWT提供了高层次的混淆,从而可以将这个问题降低到一定程度,但是仍然存在次要攻击点:GWT客户机及其服务之间的HTTP通信量。
假设我是一个攻击者,想利用Weather Reporter应用程序的弱点。图6显示了Microsoft的Fiddler工具,它拦截了从Weather Reporter客户机到运行在服务器之上的WeatherService的请求。拦截到请求之后,Fiddler允许对请求的任意部分进行修改。高亮的文本显示了我找到的指定ZIP代码在请求中的编码位置。现在我可以将ZIP代码更改为任何我喜欢的值,大致范围是从“10001”到“XXXXX”。
图6. 用Fiddler绕开客户端验证
现在,假设YahooWeatherServiceImpl中有一些服务器端代码对ZIP代码调用了Integer.parseInt()。ZIP代码最终一定会通过集成到Weather的validateAndSubmit()方法中的验证检查。正如已经看到的那样,这个检查已经被破坏,抛出了一个NumberFormatException。
在这个示例中,没有发生什么可怕的事情,攻击者只是在客户机上看到了一条错误消息。但是,对于处理更敏感数据的GWT应用程序进行全面攻击也是有可能的。假设ZIP代码被替换成了订单跟踪应用程序中的客户ID号码。拦截和修改这个值可能暴露其他客户的敏感财务信息。在数据库查询可以使用数据值的任何地方,同样的方式都有可能导致SQL注入攻击。
对于以前曾经使用过Ajax应用程序的人来说,这些不应是天方夜谭。只需要双击任何输入值,就可以在服务器上重新验证它们。关键是要记住:在GWT应用程序中编写的一些Java代码在运行时实际上是不可信任的。但是,确实还有一线希望可以解决这个GWT问题。在Weather Reporter应用程序中,我编写了一个在客户机上使用的ZipCodeValidator,可以将它移入client.common包,并在服务器端重用相同的验证。清单9显示了集成到YahooWeatherServiceImpl中的这个检查程序:
清单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调用本机JavaScript
可视效果库在Web应用程序开发中变得越来越流行,不论它们的效果只是用来提供细微的用户交互线索还是仅仅用于装饰。我想给Weather Reporter应用程序添加一些吸引眼球的东西。GWT没有提供这类功能,但是它的JavaScript本机接口(JSNI)提供了解决方案。JSNI允许直接在GWT客户机Java代码中进行JavaScript调用。这意味着我可以利用来自Scriptaculous库的效果(请参阅参考资料)或来自Yahoo!用户界面库的效果。
JSNI巧妙地把Java语言的native关键字和嵌入特殊注释块中的JavaScript组合在一起。用示例对此进行解释可能是最好的方法,所以清单10显示了一个方法,该方法调用了Element上的指定Scriptaculous效果:
清单10. 用JSNI调用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所示:
清单11. 用JSNI回调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()方法已经被更改为将额外的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用于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开发人员论坛上的讨论(请参阅参考资料)。
我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。
我原创,你原创,我们的内容世界才会更加精彩!
【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】
微信公众号
TechTarget
官方微博
TechTarget中国
相关推荐
-
多应用程序开发:挑战ALM
许多企业正在软件开发中经历同时开发多个软件的阶段,但怎样为业务应用程序开发制订短期和长期计划,以帮助他们的公司变得更成熟或为扩张累计能量?
-
享受热部署:java web应用程序
所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件。
-
Oracle发布Java移动开发框架ADF
经过两年的制作,甲骨文发布了一个移动客户端和相关框架,以帮助开发人员快速构建工业移动设备的Java应用程序。
-
细说MVC框架的几大困惑
现在许许多多的初学者和程序员,都在趋之若鹜地学习Web开发的宝典级框架:Struts2、Spring、Hibernate。