bai's log
来源: BlogBus 原始链接: http://bailog.blogbus.com:80/s1042863/index.html 存档链接: https://web.archive.org/web/20060223115251id_/http://bailog.blogbus.com:80/s1042863/index.html
bai's log 专注于spring,hibernate,Axis,web2.0的技术笔记. 2006-01-06 面向Java开发人员的Ajax: 结合Direct Web Remoting使用Ajax TAG: Java AJAX dwr 级别: 中级 Philip McCarthy , 软件开发顾问, Independent Consultant 2005 年 12 月 27 日 虽然令人兴奋,但是把 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 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 方法的配置
// 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 。 回页首 实现购物车 DWR 的安全性 DWR 设计时就考虑了安全性。使用 dwr.xml 明确地列出那些想做远程处理的类和方法,可以避免意外地把那些可能被恶意利用的功能公开出去。除此之外,使用调试测试模式,可以容易地审计所有公开到 Web 上的类和方法。 DWR 也支持基于角色的安全性。通过 bean 的 creator 配置,可以指定用户访问特定 bean 所必须属于的 J2EE 角色。通过部署多个 URL 受保护的 DWRServlet 实例,每个实例都有自己的 dwr.xml 配置文件,也可以提供拥有不同远程功能的用户集。 用户购物车的 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 类
// '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 中,可以在一个 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() 作为一个 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 的文章。 回页首 下载 描述 名字 大小 下载方法 DWR source code j-ajax3dwr.zip 301 KB FTP 关于下载方法的信息 获取 Adobe® Reader® 回页首 参考资料 学习 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。 “ Call SOAP Web services with AJAX, Part 1: Build the Web services client ” (James Snell,developerWorks,2005 年 10 月):用 Ajax 实现 SOAP Web 服务客户机。 “ 面向 Java 开发人员的 Ajax: Ajax 的 Java 对象序列化 ” (Philip McCarthy,developerWorks,2005 年 10 月):学习在 Ajax 应用程序中进行数据序列化的技术。 “ 面向 Java 开发人员的 Ajax: 构建动态的 Java 应用程序 ” (Philip McCarthy,developerWorks,2005 年 9 月):使用 Ajax 的第一步。 DWR Handbook :对 Ajax 和 DWR 的介绍。 “ Developing AJAX Applications the Easy Way ” (Java.net,2005 年 8 月):DWR 的创造者 Joe Walker 介绍如何编写简单的 Ajax 聊天服务器。 请参阅教程 An alternative to generating Ajax code with Java is using the Simple Ajax Toolkit (Sajax) ,学习如何从 Sajax 起步。 Java 技术专区 :数百篇关于 Java 编程各方面的文章。 获得产品和技术 下载 DWR :得到最新的 DWR jar 发行包。 JSON-RPC-Java :用于 Java 的另一个 Ajax-RPC 引擎。 讨论 加入本文的 论坛 。(您也可以通过点击文章顶部或者底部的论坛链接参加讨论。) DWR's Java.net pages :订阅 DWR 的邮件列表。 developerWorks blogs :加入 developerWorks 社区。 回页首 关于作者 Philip McCarthy 是一位软件开发顾问,专攻 Java 和 Web 技术。他目前在位于 Bristol 的 HP 试验室从事 Hewlett Packard 数字媒体平台的工作 。在最近几年中,Phil 开发了多个采用异步服务器通信和 DOM 脚本的富 Web 客户端。他很高兴我们现在有了一个针对它们的名称。可以通过 Phil 的电子邮件 philmccarthy@gmail.com 与他联系 bzshow @ 14:18:58 | 阅读全文 | 评论 0 | 引用 0 | 编辑 分页 : 访问统计: