虽然令人兴奋,但是把Ajax功能添加到应用程序可能意味着大量的艰苦工作。在面向Java开发人员的Ajax系列的第3篇文章中,Philip McCarthy介绍了如何使用Direct Web Remoting(DWR)直接把JavaBean的方法公开给JavaScript代码并自动进行Ajax的繁重工作。
理解Ajax编程的基本知识是重要的,但是如果正在构建复杂的用户界面,那么能够在更高层次的抽象上工作也很重要。在面向Java开发人员的Ajax系列的第3篇文章中,我在上个月的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类上的对应方法并在幕后执行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显示了实现这一功能的单元格函数数组:
清单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的函数。这个函数是第二个用例的入口点:向购物车中添加Item。
实现购物车
用户购物车的Java表示基于Map。当Item添加到购物车中时,Item本身作为键被插入Map。 Map中对应的值是一个Integer,代表购物车中指定Item的数量。所以Cart.java有一个字段contents,声明为Map<Item,Integer>。
使用复杂类型作为哈希键给DWR带来一个问题——在JavaScript中,数组的键必须是标量的。所以,DWR无法转换contents Map。但是,对于购物车用户界面来说,用户需要查看的只是每个商品的名称和数量。所以我向Cart添加了一个名为getSimpleContents()的方法,它接受contents Map并根据它构建一个简化的Map<String,Integer>,只代表每个 Item 的名称和数量。这个用字符串作为键的map表示可以由DWR的转换器转换成 JavaScript。
客户对Cart感兴趣的其他字段是totalPrice,它代表购物车中所有商品的金额汇总。使用Item,我还提供了一个合成的成员叫作formattedTotalPrice,它是金额汇总的格式化好的String表示。
转换购物车
为了不让客户代码对Cart做两个调用(一个获得内容,一个获得总价),我想把这些数据一次全都发给客户。为了做到这一点,我添加了一个看起来有点儿怪的方法,如清单5所示:
清单 5. Cart.getCart()方法
/**
* Returns the cart itself – for DWR
* @return the cart
*/
public Cart getCart() {
return this;
}
虽然这个方法在普通的Java代码中可能完全是多余的(因为在调用这个方法时,已经有对Cart的引用),但它允许DWR客户让Cart把自己序列化成JavaScript。
除了getCart(),需要远程化的另一个方法是addItemToCart()。这个方法接受目录Item的ID的String表示,把这个商品添加到Cart中并更新总价。方法还返回Cart,这样客户代码在一个操作中就能更新Cart的内容并接收购物车的新状态。
清单6是扩展的dwr.xml配置文件,包含Cart类进行远程所需要的额外配置:
清单6. 修改过的dwr.xml包含了Cart类
<!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的convert元素是必需的,因为远程的Cart方法返回的是Cart本身。在这里我指定在Cart的序列化JavaScript形式中应当存在的成员是simpleContents这个图和formattedTotalPrice这个字符串。
如果对这觉得有点儿不明白,那么只要记住create元素指定的是DWR客户可以调用的Cart服务器端方法,而convert元素指定在Cart的JavaScript序列化形式中包含的成员。
现在可以实现调用Cart的远程方法的客户端代码了。
调用远程的Cart方法
首先,当商店的Web页首次装入时,我想检查保存在会话中的Cart的状态,看是否已经有一个购物车了。这是必需的,因为用户可能已经向Cart中添加了商品,然后刷新了页面或者导航到其他地方之后又返回来。在这些情况下,重新载入的页面需要用会话中的Cart数据对自己进行同步。我可以在页面的onload函数中用一个调用做到这一点,就像这样:Cart.getCart(displayCart)。请注意displayCart()是一个回调函数,由服务器返回的Cart响应数据调用。
如果Cart已经在会话中,那么creator会检索它并调用它的getCart()方法。如果会话中没有Cart,那么creator会实例化一个新的,把它放在会话中,并调用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的状态更新用户界面:
清单8. displayCart()实现
/*
* 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是一个把String映射到数字的JavaScript数组。每个字符串都是一个商品的名称,关联数组中的对应数字就是购物车中该商品的数量。所以表达式cart.simpleContents[item] + ” x ” + item可能就会计算出“2 x Oolong 128MB CF Card”这样的结果。
DWR商店应用程序
图3显示了这个基于DWR的Ajax应用程序的使用情况:显示了通过搜索检索到的商品,并在右侧显示用户的购物车:
图3. 基于DWR的Ajax商店应用程序的使用情况
DWR的利弊
现在可以看出用DWR实现由Java支持的Ajax应用程序有多么容易了。虽然示例场景很简单,我实现用例的手段也尽可能少,但是不应因此而低估DWR引擎相对于自己设计Ajax应用程序可以节约的工作量。在前一篇文章中,我介绍了手工设计Ajax请求和响应、把Java对象图转化成JSON表示的全部步骤,在这篇文章中,DWR替我做了所有这些工作。我只编写了不到50行JavaScript就实现了客户机,而在服务器端,我需要做的所有工作就是给常规的JavaBean加上一些额外方法。
当然,每种技术都有它的不足。同任何RPC机制一样,在DWR中,可能很容易忘记对于远程对象进行的每个调用都要比本地函数调用昂贵得多。DWR在隐藏Ajax的机械性方面做得很好,但是重要的是要记住网络并不是透明的——进行DWR调用会有延迟,所以应用程序的架构应当让远程方法的粒度比较粗。正是为了这个目的,addItemToCart()才返回Cart本身。虽然让addItemToCart()作为一个void方法可能更自然,但是这样的话对它的每个DWR调用后面都必须跟着一个getCart()调用以检索修改后的Cart状态。
对于延迟,DWR在调用的批处理中有自己的解决方案(请参阅侧栏的调用批处理)。如果不能为应用程序提供适当粗粒度的Ajax接口,那么只要有可能把多个远程调用组合到一个HTTP请求中,就请使用调用批处理。
分离的问题
从实质上看,DWR在客户端和服务器端代码间形成了紧密的耦合,这有许多含义:首先,远程方法API的变化需要在DWR存根调用的JavaScript上反映出来。第二(也是最明显的),这种耦合会造成对客户端的考虑会渗入服务器端代码。例如,因为不是所有Java类型都能转化成 JavaScript,所以有时有必要给Java对象添加额外方法,好让它能够更容易地远程化。在示例场景中,我通过把getSimpleContents()方法添加到 Cart 来解决这个问题。我还添加了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是一个快速发展的领域,不断有新技术涌现。在这个系列的三篇文章中,我的重点在于带您开始在Ajax应用程序的Web层中利用Java技术——不管是选择基于XMLHttpRequest的带有对象序列化框架的技术,还是选择DWR这样的更高级抽象。请在后续几个月中留意面向Java开发人员介绍Ajax的文章。
我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。
我原创,你原创,我们的内容世界才会更加精彩!
【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】
微信公众号
TechTarget
官方微博
TechTarget中国
相关推荐
-
AWS MEAN堆栈+JavaScript=快速搭建应用
开发人员在构建Web应用时有许多选择。市面上有无数的框架和语言可选,而像AWS这样的云平台可以方便地部署和扩展应用程序。
-
内存数据网格提供商一头扎进Java
10年的时间里,应用性能解决方案提供商Alachisoft一直在用NCache(针对N-Tier和网格计算.NET应用的内存计算和数据网格产品)为.NET社区服务。
-
遇到这样一个问题:通过java service wrapper部署应用,wrapper进程占用的内存会一直升高, 直到把内存吃完应用崩溃,但是这个wrapper
遇到这样一个问题:通过java service wrapper部署应用,wrapper进程占用的内存会一直升高 […]
-
Google App Engine for Java 对于目前中国需要学习吗?