DWR 简介
从最简单的角度来说,DWR 是一个引擎,可以把服务器端 Java 对象的方法公开给 JavaScript 代码。使用 DWR 可以有效地从应用程序代码中把 Ajax 的全部请求-响应循环消除掉。这意味着客户端代码再也不需要直接处理 XMLHttpRequest 对象或者服务器的响应。不再需要编写对象的序列化代码或者使用第三方工具才能把对象变成 XML。甚至不再需要编写 servlet 代码把 Ajax 请求调整成对 Java 域对象的调用。
DWR 是一个开放源码的使用 Apache 许可协议的解决方案,它包含服务器端 Java 库、一个 DWR servlet 以及 JavaScript 库。虽然 DWR 不是 Java 平台上唯一可用的 Ajax-RPC 工具包,但是它是最成熟的,而且提供了许多有用的功能。
DWR 是作为 Web 应用程序中的 servlet 部署的。把它看作一个黑盒子,这个 servlet 有两个主要作用:首先,对于公开的每个类,DWR 动态地生成包含在 Web 页面中的 JavaScript。生成的 JavaScript 包含存根函数,代表 Java 类上的对应方法并在幕后执行 XMLHttpRequest。这些请求被发送给 DWR,这时它的第二个作用就是把请求翻译成服务器端 Java 对象上的方法调用并把方法的返回值放在 servlet 响应中发送回客户端,编码成 JavaScript。DWR 还提供了帮助执行常见的用户界面任务的 JavaScript 工具函数。
关于示例
在更详细地解释 DWR 之前,我要介绍一个简单的示例场景。像在前一篇文章中一样,我将采用一个基于在线商店的最小模型,这次包含一个基本的产品表示、一个可以包含产品商品的用户购物车以及一个从数据存储查询产品的数据访问对象(DAO)。Item 类与前一篇文章中使用的一样,但是不再实现任何手工序列化方法。图 1 说明了这个简单的设置:
图 1. 说明 Cart、CatalogDAO 和 Item 类的类图
在这个场景中,我将演示两个非常简单的用例。第一,用户可以在目录中执行文本搜索并查看匹配的商品。第二,用户可以添加商品到购物车中并查看购物车中商品的总价。
实现目录
DWR应用程序的起点是编写服务器端对象模型。在这个示例中,我从编写 DAO 开始,用它提供对产品目录数据存储的搜索功能。CatalogDAO.java 是一个简单的无状态的类,有一个无参数的构造函数。清单 1 显示了我想要公开给 Ajax 客户的 Java 方法的签名:
清单 1. 通过 DWR 公开的 CatalogDAO 方法
/**
* Returns a list of items in the catalog that have
* names or descriptions matching the search expression
* @param expression Text to search for in item names
* and descriptions
* @return list of all matching items
*/
public List<Item> findItems(String expression);
/**
* Returns the Item corresponding to a given Item ID
* @param id The ID code of the item
* @return the matching Item
*/
public Item getItem(String id);
接下来,我需要配置 DWR,告诉它 Ajax 客户应当能够构建 CatalogDAO 并调用这些方法。我在清单 2 所示的 dwr.xml 配置文件中做这些事:
清单 2. 公开 CatalogDAO 方法的配置
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
"http://www.getahead.ltd.uk/dwr/dwr10.dtd">
<dwr>
<allow>
<create creator="new" javascript="catalog">
<param name="class"
value="developerworks.ajax.store.CatalogDAO"/>
<include method="getItem"/>
<include method="findItems"/>
</create>
<convert converter="bean"
match="developerworks.ajax.store.Item">
<param name="include"
value="id,name,description,formattedPrice"/>
</convert>
</allow>
</dwr>
dwr.xml 文档的根元素是 dwr。在这个元素内是 allow 元素,它指定 DWR 进行远程的类。allow 的两个子元素是 create 和 convert。
create 元素
create 元素告诉 DWR 应当公开给 Ajax 请求的服务器端类,并定义 DWR 应当如何获得要进行远程的类的实例。这里的 creator 属性被设置为值 new,这意味着 DWR 应当调用类的默认构造函数来获得实例。其他的可能有:通过代码段用 Bean 脚本框架(Bean Scripting Framework,BSF)创建实例,或者通过与 IOC 容器 Spring 进行集成来获得实例。默认情况下,到 DWR 的 Ajax 请求会调用 creator,实例化的对象处于页面范围内,因此请求完成之后就不再可用。在无状态的 CatalogDAO 情况下,这样很好。
create 的 javascript 属性指定从 JavaScript 代码访问对象时使用的名称。嵌套在 create 元素内的 param 元素指定 creator 要创建的 Java 类。最后,include 元素指定应当公开的方法的名称。显式地说明要公开的方法是避免偶然间允许访问有害功能的良好实践 —— 如果漏了这个元素,类的所有方法都会公开给远程调用。反过来,可以用 exclude 元素指定那些想防止被访问的方法。
convert元素
creator负责公开用于 Web 远程的类和类的方法,convertor 则负责这些方法的参数和返回类型。convert 元素的作用是告诉 DWR 在服务器端 Java 对象表示和序列化的 JavaScript 之间如何转换数据类型。
DWR自动地在Java和JavaScript 表示之间调整简单数据类型。这些类型包括Java原生类型和它们各自的类表示,还有 String、Date、数组和集合类型。DWR 也能把 JavaBean 转换成 JavaScript 表示,但是出于安全性的原因,做这件事要求显式的配置。
清单2中的 convert 元素告诉 DWR 用自己基于反射的 bean 转换器处理 CatalogDAO 的公开方法返回的 Item,并指定序列化中应当包含 Item 的哪个成员。成员的指定采用 JavaBean 命名规范,所以 DWR 会调用对应的 get 方法。在这个示例中,我去掉了数字的 price 字段,而是包含了 formattedPrice 字段,它采用货币格式进行显示。
现在,我准备把 dwr.xml 部署到 Web 应用程序的 WEB-INF 目录,在那里 DWR servlet 会读取它。但是,在继续之前,确保每件事都按照希望的那样运行是个好主意。
测试部署
如果DWRServlet的web.xml定义把 init-param debug 设置为 true,那么就启用了 DWR 非常有帮助的测试模式。导航到 /{your-web-app}/dwr/ 会把 DWR 配置的要进行远程的类列表显示出来。在其中点击,会进入指定类的状态屏幕。CatalogDAO 的 DWR 测试页如图 2 所示。除了提供粘贴到 Web 页面的 script 标记(指向 DWR 为类生成的 JavaScript)之外,这个屏幕还提供了类的方法列表。这个列表包括从类的超类继承的方法,但是只有在 dwr.xml 中显式地指定为远程的才标记为可访问。
图 2. CatalogDAO 的 DWR 测试页
可以在可访问的方法旁边的文本框中输入参数值并点击 Execute 按钮调用方法。服务器的响应将在警告框中用 JSON 标注显示出来,如果是简单值,就会内联在方法旁边直接显示。这个测试页非常有用。它们不仅允许检查公开了哪个类和方法用于远程,还可以测试每个方法是否像预期的那样工作。
如果对远程方法的工作感到满意,就可以用 DWR 生成的 JavaScript 存根从客户端代码调用服务器端对象。
调用远程对象
远程 Java 对象方法和对应的 JavaScript 存根函数之间的映射很简单。通用的形式是 JavaScriptName.methodName(methodParams ..., callBack),其中 JavaScriptName 是 creator 的 javascript 属性指定的名称,methodParams 代表 Java 方法的 n 个参数,callback 是要用 Java 方法的返回值调用的 JavaScript 函数。如果熟悉 Ajax,可以看出这个回调机制是 XMLHttpRequest 异步性的常用方式。
在示例场景中,我用清单 3 中的 JavaScript 函数执行搜索,并用搜索结果更新用户界面。这个清单还使用来自 DWR 的 util.js 的便捷函数。要特别说明的是名为 $() 的 JavaScript 函数,可以把它当作 document.getElementById() 的加速版。录入它当然更容易。如果您使用过 JavaScript 原型库,应当熟悉这个函数。
清单 3. 从客户机调用远程的 findItems()
/*
* Handles submission of the search form
*/
function searchFormSubmitHandler() {
// Obtain the search expression from the search field
var searchexp = $("searchbox").value;
// Call remoted DAO method, and specify callback function
catalog.findItems(searchexp, displayItems);
// Return false to suppress form submission
return false;
}
/*
* Displays a list of catalog items
*/
function displayItems(items) {
// Remove the currently displayed search results
DWRUtil.removeAllRows("items");
if (items.length == 0) {
alert("No matching products found");
$("catalog").style.visibility = "hidden";
} else {
DWRUtil.addRows("items",items,cellFunctions);
$("catalog").style.visibility = "visible";
}
}
在上面的 searchFormSubmitHandler() 函数中,我们感兴趣的代码当然是 catalog.findItems(searchexp, displayItems);。这一行代码就是通过网络向 DWR servlet 发送 XMLHttpRequest 并用远程对象的响应调用 displayItems() 函数所需要的全部内容。
displayItems() 回调本身是由一个 Item 数组表示调用的。这个数组传递给 DWRUtil.addRows() 便捷函数,同时还有要填充的表的 ID 和一个函数数组。表中每行有多少单元格,这个数组中就有多少个函数。按照顺序使用来自数组的 Item 逐个调用每个函数,并用返回的内容填充对应的单元格。
在这个示例中,我想让商品表中的每一行都显示商品的名称、说明和价格,并在最后一列显示商品的 Add to Cart 按钮。清单 4 显示了实现这一功能的单元格函数数组: