最近发布的 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 源代码:
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),与一个简单的浏览器组合在一起。
![]() ![]() |
![]()
|
我将用 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 项目符合这个结构。(请参阅 下载)。
首先,我将开发应用程序的用户界面小部件(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 中呈现的布局:
呈现的 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 显示了应用程序被替换成这些样式之后的情况:
现在应用程序的基本布局和样式已经就绪,我将开始实现一些客户端行为。可以用熟悉的侦听器模式在 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 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()
方法,这个方法稍后再介绍。
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,它拥有允许在 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 的 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
的异步接口:
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 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”。
现在,假设 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"); } |
![]() ![]() |
![]()
|
可视效果库在 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 所示:
/** * 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 应用程序显然不太类似于 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 开发人员论坛上的讨论(请参阅 参考资料)。