Java 开发者的Ajax技术: 使用Direct Web Remoting的Ajax
原文地址 : http://www-128.ibm.com/developerworks/java/library/j-ajax3/
没有比这更简单的数据串行化方法了!
水平:中等
Philip McCarthy (philmccarthy@gmail.com),软件开发顾问,独立顾问
2005-11-8
在你的应用程序中加入Ajax的功能在给你带来兴奋的同时,也意味着一大堆辛苦的工作。在 这 “Java 开发者的Ajax技术”系列的第三篇文章中,菲利普•麦卡锡向你显示了如何使用Direct Web Remoting (DWR)来让你在JavaScript的代码中直接使用JavaBean的方法并且使得Ajax的沉重工作自动化。
理解Ajax编程的基本原理是必要的。但是如果你要实现复杂的Ajax用户界面,能在较高层抽象的基础上工作也是很重要的方面。在 这 “Java 开发者的Ajax技术”系列的第三篇文章中,我会在上个月对Ajax数据串行化技术的介绍的基础上,引入一种技术来让你绕过串行化Java对象的细节。
在上一篇文章中,我向你显示了如何用JavaScript对象符号(JSON)来按照先前在客户端转换到JavaScript对象中的格式序列化数据。在这个基础上,你可以用JavaScript的代码调用远程服务并且在应答中获得JavaScript对象图,和远程过程调用类似。这一次,你会学到更进一步的东西,使用一个框架,它规范了你在JavaScript的客户端代码中调用服务器端的Java 对象的方法。
DWR是开源代码,由Apache 发放许可的解决方案。它由服务端的Java库、一个DWR servlet和JavaScript库组成。虽然DWR不是Java平台上唯一可用的Ajax-RPC工具,但是它是最成熟的一个,而且它提供了许多实用的功能。在按照文中的例子继续之前先到文章末尾的资源链接中下载DWR吧。
什么是DWR?
用最简单的话说,DWR是让服务端的Java对象的方法暴露在JavaScript代码中的一个引擎。DWR可以有效地使你在应用程序代码中忽略Ajax的请求-应答圈的全部机制。这意味着你的客户端代码中从不需要直接处理XMLHttpRequest对象和服务端响应。你不需要写对象序列化代码或使用第三方工具来把你的对象转成XML。你甚至不需要写servlet代码来把Ajax请求转成对你的Java领域的对象的调用。
DWR 在你的Web应用程序中被部署为一个servlet。这个servlet被看成一个黑盒,它主要扮演两个角色:一、对每个暴露的类,DWR动态生成了要被包含在你的Web页中的JavaScript。这个生成的JavaScript包含表示Java类的相应方法的存根函数(stub function),并且在幕后完成XMLHttpRequest的任务。这些请求被发给DWR servlet。作为第二个角色,servlet把请求转为对服务端的Java对象的方法的调用并且把调用的返回值(编码为Javascript)放在servlet的响应中发还给客户端。DWR还提供Javascript实现的函数来协助一般的用户界面工作。
关于例子
在详细解释DWR之前,我会引入一个简单的例子。就像在前面的文章一样,我要使用在线商店基础上的小模型,这次由基本商品展示、用户购物车(用来装商品)和数据访问对象(从数据存储中查找商品细节信息)几部分组成。物品类(Item class)和前面文章中使用的一样,但它不再实现手工串行化的方法了。图1描述了这个简单关系:
图1. 购物车(Cart)、目录DAO(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来完成这个功能:
|
<!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脚本框架(BSF)的脚本片段,或是通过IOC容器和Spring的综合技术。在默认情况下,当提交给DWR的Ajax请求调用了一个构造函数,这个实例化的对象被放置到页面范围内,因此在请求完成后不再有效。在无状态的CatalogDAO看来,这样是好的。
create的javascript属性指定了Javascript代码要访问对象时需要使用的名字。嵌套在create元素中的,一个param元素指定了构造者要创建的Java类。最后,include元素指定了要暴露的方法的名字。明确地指出要暴露的方法是避免偶然允许潜在的有害功能的好主意。如果这个元素被忽略了,所有的类方法都会暴露给远程调用。相对地,你可以使用exclude元素来指定不允许访问的方法。
convert 元素
就像creator属性和Web远程调用的暴露的类和它们的方法有关,converter就与那些方法的参数和返回类型有关。convert元素用来告诉DWR怎样在服务端的Java对象表示和序列化的Javascript表示之间转换数据类型。
DWR自动地在Java和Javascript 之间转换简单的数据类型。这些类型包括Java基本类型和它们各自的类描述,还有字符串、日期、数组和集合类型。DWR还可以把JavaBean转为Javascript描述,但是由于安全的原因,这样做需要明确的配置。
列表2中的convert元素告诉DWR去用它自己的基于反射的bean convertor来转化暴露的CatalogDAO的方法返回的项目,并指定了哪些项目的成员可以被包含在序列化中。这些成员遵从用JavaBean的命名习惯,所以DWR可以调用相应的get方法。在这个例子里,我忽略了数字表示的price成员,而包含了formattedPrice成员来取代它。formattedPrice是可以直接用来显示的货币类型。
到了这里,我已经准备好部署我的dwr.xml到我的Web程序的WEB-INF的目录,在那里DWR的servlet会找到它。在进一步继续之前,无论如何,确认所有东西都能像想的那样工作是个好主意。
测试发布
如果DWR servlet 的web.xml定义设置初始参数debug为真,那么DWR的很有用的测试模式就开启了。浏览/{your-web-app}/dwr/带来的DWR向远程发布的类的列表,一路单击来到一个给定类的状态页面。图2中显示的是关于CatalogDAO的DWR测试页面。就像提供script标签嵌到你的Web页面中去一样,转向指到DWR给这个类生成的JavaScript的链接上,页面会显示这个类的方法列表。列表中包括从超类中继承来的方法。但只有在dwr.xml明确指定可以远程调用的方法是可以访问的。
图 2. CatalogDAO 的DWR 测试页面
可以在可访问的方法旁边的文本框中输入参数值然后点击Execute按钮来调用它们。服务器的响应内容会在警告框中用JSON符号显示出来,简单值的响应直接在方法的旁边显示。这些测试页面非常有用。它们不仅让你能够简单地检查哪些类和方法可供远程访问,你还可以测试每个方法是否像预期地那样工作。
直到你对你的远程方法的正确工作结果感到满意时,你就可以使用DWR生成的Javascript存根函数在客户端代码中调用服务端的对象。
调用远程对象
远程Java对象方法和它们相应的JavaScript存根函数之间的映射是简单的。生成JavaScript的形式是JavaScriptName.methodName(methodParams ..., callBack),其中JavaScriptName是creator的javascript 属性指定的名字,methodParams表示Java方法的n个参数,callBack是一个JavaScript函数,它被调用时能得到Java方法的返回值。如果你对Ajax熟悉的话,你会认出这个回调机制,它和XMLHttpRequest中的异步机制类似。
在这个例子的背景下我使用列表3中的JavaScript函数来实现搜索并用搜索的结果更新用户界面。这个列表也使用了DWR的 util.js中的方便的函数。值得特别注意的是叫$()的JavaScript函数,它可以看作document.getElementById()的一个改编版本。显然它更容易写出来。如果你使用过prototype JavaScript library,你会对这个函数很熟悉。
列表 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);。这一行代码就是所有用来发送XMLHttpRequest给DWR servlet并在远程对象的响应中调用displayItems()函数的代码了。
displayItems()回调函数被调用时有个Item数组参数。这个数组跟放置物品的表格ID和一组函数一起被传给DWRUtil.addRows()函数。在这个数组中的函数数量跟表格中每行的单元数量一样。每个函数都和数组中的Item一起轮流被调用,返回相应的单元格中要填充的内容。
在这个例子中,我让表格中关于物品的每行都要显示物品的名字、描述和价格,在最后一列还有一个Add to Cart按钮。列表4显示了实现这个功能的单元函数数组:
|
/*
* Array of functions to populate a row of the items table
* using DWRUtil's addRows function
*/
var cellFunctions = [
function(item) { return item.name; },
function(item) { return item.description; },
function(item) { return item.formattedPrice; },
function(item) {
var btn = document.createElement("button");
btn.innerHTML = "Add to cart";
btn.itemId = item.id;
btn.onclick = addToCartButtonHandler;
return btn;
}
]; |
前三个函数简单地返回dwr.xml中的Item的convertor说明的成员。最后的一个函数创建了一个按钮,把Item的ID赋值给它,并说明了当按钮被点击时需要调用一个叫addToCartButtonHandler的函数。这个函数是第二个用例的入口:将一个物品加入到购物车中。
实现购物车
|
DWR的安全性
DWR在设计时就考虑了安全问题。在dwr.xml中只把把想暴露给远程的类和方法加入到白名单中,这样避免了不经意地暴露可能被策划用来攻击的功能。另外,在调试测试模式下,很容易就能审核暴露给Web的所有类和方法。
DWR还支持基于角色的安全机制。通过bean的 creator配置,可以指定只有某个J2EE角色才能访问这个bean。通过部署多个安全DWR Servlet实例的多个URL(每个Servlet都有自己的dwr.xml配置文件),你可以定义有不同远程功能调用的用户集合。 |
用户的购物车的Java表示是基于一个映射的。当一个物品加到车中时,物品就被插入到映射中作为关键字。映射中有个相应的整型值表示车中物品的数量。因此Cart.java有个成员被声明为Map<Item,Integer>。
使用复杂类型当作杂凑关键字给DWR提了一个难题——在Javascript中,数组的索引必须是直接量。于是,内容映射不能被DWR转化成它自己的样子。但不管怎样,为了完成购物车的用户界面。用户需要看到的所有东西只是车中物品的名字和数量。所以给Cart加一个方法叫getSimpleContents(),它接受内容映射,按照它建立一个简单的映射<String,Integer>,仅仅表示每个物品的名字和数量。这个字符串索引的映射表示能很容易地被DWR的内建转换器转成Javascript。
Cart的另一个客户关心的字段是总价格totalPrice,表示购物车中所有物品的价格总和。跟物品Item在一起,我提供了一个合成的成员formattedTotalPrice,它是一个预定义了格式的字符串,表示数字总和。
购物车的转化
对于在客户端使用两个Cart的调用(一个用来得到内容,一个用来得到总价格),我更想一次就把所有数据都送到客户端。为了达到这个目的,我加了一个看上去很奇怪的方法,如下面列表5显示的:
|
/**
* Returns the cart itself - for DWR
* @return the cart
*/
public Cart getCart() {
return this;
} |
在普通的Java代码中这个方法是完全多余的(因为你调用这个方法的时候你已经有一个Cart的引用了)。这个方法使得DWR客户可以把Cart它自己序列化为JavaScript。
除getCart()以外,另一个需要远程调用的方法是addItemToCart()。这个方法有一个字符串参数表示物品的ID,它把物品加到购物车中,更新总价格。这个方法也返回Cart,以便客户端代码可以更新车的内容并在一个操作中得到它的新状态。
列表6是扩展的dwr.xml配置文件,它包括关于远程类Cart的额外配置:
列表 6. 修改过的包含了Cart类的 dwr.xml
|
<!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>
</create creator="new" scope="session" javascript="Cart">
</param name="class"
value="developerworks.ajax.store.Cart"/>
</include method="addItemToCart"/>
</include method="getCart"/>
<//create>
</convert converter="bean"
match="developerworks.ajax.store.Cart">
</param name="include"
value="simpleContents,formattedTotalPrice"/>
<//convert>
<//allow>
</dwr> |
在这个版本的dwr.xml中,我为Cart加了一个creator和一个convertor。create元素指定了addItemToCart() 和 getCart()方法可以远程调用,并且,很重要的,创建的Cart实例要被放在用户的会话中。于是,cart的内容生存于用户的请求中。
关于Cart的convert元素是必须的,因为远程的Cart的方法返回了Cart自己。这里我指定要在序列化的Javascript中可用的Cart的成员是映射simpleContents和字符串formattedTotalPrice。
如果你觉得这有一点让人迷惑,请注意create元素指定了DWR客户端可以调用的Cart服务端方法,而convert元素指定了要包含在Cart的Javascript序列化的成员。
现在我可以实现客户端代码,调用我的远程Cart方法了。
远程调用Cart方法
首先,当商店的Web页第一次装载时,我要检查会话session中存储的Cart的状态,如果有的话。这是必须的,因为用户可能已经把物品放到购物车中,然后刷新了页面,或浏览了别处以后又回来。在这些情况下,重载的页面需要用会话session中的Cart数据让自己保持同步。我可以用页面的onload函数来完成这个功能,像这样: Cart.getCart(displayCart) 。注意displayCart()是一个回调函数,被调用时带有服务端的Cart响应数据。
如果Cart已经在会话session里了,构造者会重新得到它并调用它的getCart()方法。如果会话session里没有Cart,构造者会新实例化一个,放在会话session中,并调用getCart()方法。
列表7显示了addToCartButtonHandler()函数的实现。当物品的Add to Cart按钮被点击时这个函数被调用:
列表 7. addToCartButtonHandler() 的实现
|
/*
* Handles a click on an Item's "Add to Cart" button
*/
function addToCartButtonHandler() {
// 'this' is the button that was clicked.
// Obtain the item ID that was set on it, and
// add to the cart.
Cart.addItemToCart(this.itemId,displayCart);
} |
有了DWR来管理所有的交流通讯,客户端的“加入购物车”动作的代码只有一行函数。列表8显示了这个七巧板形状的最后一片——displayCart()回调的实现,它用Cart的状态更新用户界面:
|
/*
* Displays the contents of the user's shopping cart
*/
function displayCart(cart) {
// Clear existing content of cart UI
var contentsUL = $("contents");
contentsUL.innerHTML="";
// Loop over cart items
for (var item in cart.simpleContents) {
// Add a list element with the name and quantity of item
var li = document.createElement("li");
li.appendChild(document.createTextNode(
cart.simpleContents[item] + " x " + item
));
contentsUL.appendChild(li);
}
// Update cart total
var totalSpan = $("totalprice");
totalSpan.innerHTML = cart.formattedTotalPrice;
} |
这里注意simpleContents是一个Javascript数组,它将字符串映射为数字。每个映射的字符串是物品的名字,相应的数字是车中物品的数量。因此,cart.simpleContents[item] + " x " + itemevaluates的结果就是"2 x Oolong 128MB CF Card"(举个例子)。
DWR商店应用程序
图3显示了运行时的我的基于DWR的Ajax应用程序,显示了搜索到的物品,右边是用户的购物车:
图 3. 运行时的基于DWR的Ajax商店应用程序
DWR的优势和不足
|
批量调用
在DWR中可以把几个远程调用放在单个的HTTP请求中发送给服务端。调用 DWREngine.beginBatch()告诉DWR不要把后面的远程调用直接发出去,相反要把它们组合进单个的批请求中。调用 DWREngine.endBatch()把批调用发给服务端。远程调用在服务端按顺序调用,并且每个JavaScript回调函数都被调用。
批处理可以在两方面减少延迟:第一,你可以避免创建 XMLHttpRequest对象和为每个调用建立HTTP连接的开销。第二,在一个产品环境中,Web服务器不需要同时处理这么多HTTP请求,提高了响应次数。 |
你已经看到了用DWR来实现一个Java支持的Ajax应用程序是多么的容易。尽管这个例子很简单,而且我用了相当简化的方法去实现这个用例,但你不应该低估使用DWR引擎相对于直接用Ajax来实现所节省的工作量。在上一篇文章中我全部用手工的方式来建立Ajax请求和响应,并把一个Java对象表示转成JSON表示,而这次是DWR做了所有这些工作。我只写了不到50行的JavaScript去实现客户端,而在服务端,我所做的所有工作只是给我常规的JavaBean加了一些额外的方法。
当然,所有技术都有它的缺点。就像所有的RPC机制一样,使用DWR很容易忽视远程对象的调用的消耗要比本地函数调用要多得多。DWR做了很多工作来隐藏Ajax机制,但注意网络不是透明的——调用DWR方法是有延迟的,故你的应用程序要有合理的架构以让远程方法是粗略的。addItemToCart() 方法返回Cart自己就是为了这个目的。虽然让addItemToCart() 方法没有返回值显得更自然,但这样的话每个DWR调用addItemToCart()以后都需要再调用getCart()来获得修改过的Cart状态。
对于批量调用的延迟问题,DWR有自己的解决方法(参见右边的补充说明)。如果你不能为你的应用程序给出一个合适的粗略Ajax接口的话,可以使用批量调用,尽可能把多次远程调用组合成单个的HTTP请求。
关注隔离
很自然地,DWR在客户端和服务端代码之间建立了一个紧密连接,带来了一些隐含的效果。第一,远程方法API的改变需要反应到调用DWR存根函数的JavaScript中。第二(更意味深长地),这个紧密连接让客户端的东西渗漏到服务端的代码里。例如,因为不是所有Java类型都能被转化为JavaScript,有时候就需要在Java对象中加入额外的方法以便能更容易地从远程调用。在本文的例子中,我通过给Cart增加一个getSimpleContents()方法来解决这个问题。我也加入了getCart()方法,它在DWR背景下是有用的,但同时又是完全多余的。从对远程对象的粗略API的需求和一些Java类型转化为JavaScript的问题中,你可以看到远程的JavaBean是如何被哪些只在Ajax客户端中有用的方法污染的。
为了避免这么做,你可以使用包装类来把额外的DWR需要的方法加到你原来的简单的JavaBean中。这意味着JavaBean类的Java客户不会看到跟远程调用有关的额外的乱糟糟的东西,并且这样还能让你给远程方法的命名更加友好,例如用getPrice()取代getFormattedPrice()。图4显示了一个RemoteCart类,它包装了Cart以加入额外的DWR功能:
图 4. RemoteCart 包装了Cart 以完成远程调用功能
最后,你需要记住,DWR Ajax调用是异步的,而且不能指望它们返回的顺序和它们发送的顺序一样。在示例代码中我忽略了这个小障碍,但在这个系列的第一篇文章中,我示范了如何给响应加上时间戳作为治理数据乱序到达的一个简单的安全防护。
结论
就像你看到的,DWR提供了很多东西——它允许你简单快速地给你的服务端对象创建一个Ajax接口,并且不需要写什么servlet代码、对象序列化代码或客户端的XMLHttpRequest 代码。用DWR发布你的Web应用程序是极为简单的,而且DWR的安全特性可以和J2EE的身份验证系统集成起来。但是,DWR不是对每种应用程序架构都适用,而且它需要你思考一下你的对象API的设计。
如果你想了解更多关于使用DWR的Ajax的优势和不足,最好的方法就是下载它并自己动手开始试验。虽然DWR有很多特性我还没谈到,但是这篇文章的源代码是走上DWR之路的一个好开端。看看文后的资源链接可以了解更多关于Ajax、DWR和相关技术的内容。
这个系列文章中体现最重要的方面之一是,对于Ajax应用程序而言,没有适合所有情况的解决办法。Ajax是一个快速发展的领域,不少新技术在同时一起浮现。在这个系列的三篇文章中,我努力让你能够开始开发简单的Web层面上的Ajax应用程序——不管你是选择基于XMLHttpRequest,带有对象序列化框架的方法,还是选择高层次抽象的DWR。在接下来的几个月里找一下关于Java开发者探索Ajax的进一步的文章。
下载
| Description |
Name |
Size |
Download method |
| DWR source code |
j-ajax3dwr.zip |
301 KB |
FTP |
资源
学习
产品和技术
讨论
关于作者
Philip McCarthy是Java和Web技术领域的软件开发顾问。他现在工作于位于布里斯托的惠普的实验室,做惠普数字媒体平台项目。最近几年Phil开发了几个富Web客户端用于异步服务通讯和DOM脚本。他很高兴我们现在知道了它们的名字。你可以用这个email和Phil联系:
philmccarthy@gmail.com。