Ajax for Java developers: Ajax with Direct Web Remoting

http://www-128.ibm.com/developerworks/java/library/j-ajax3/

Level: Intermediate

Philip McCarthy ([email protected]), Software development consultant, Independent

08 Nov 2005

Exciting as it is, adding Ajax functionality to your applications can mean a lot of hard work. In this third article in the Ajax for Java™ developers series, Philip McCarthy shows you how to use Direct Web Remoting (DWR) to expose JavaBeans methods directly to your JavaScript code and automate the heavy-lifting of Ajax.

Understanding the fundamentals of Ajax programming is essential, but if you're building complex Ajax UIs, it's also important to be able to work at a higher level of abstraction. In this third article in the Ajax for Java developers series, I build on last month's introduction to data serialization techniques for Ajax, introducing a technique that will let you avoid the nitty-gritty details of serializing Java objects.

In the previous article, I showed you how to use JavaScript Object Notation (JSON) to serialize data in a format easily converted into JavaScript objects on the client. With this setup, you can invoke remote service calls using JavaScript code and receive JavaScript object graphs in response, not unlike making a remote procedure call. This time, you learn how to take things one step further, using a framework that formalizes your ability to make remote procedure calls on server-side Java objects from JavaScript client code.

DWR is an open source, Apache licensed solution consisting of server-side Java libraries, a DWR servlet, and JavaScript libraries. While DWR is not the only Ajax-RPC toolkit available for the Java platform, it is one of the most mature, and it offers a great deal of useful functionality. See Resources to download DWR before proceeding with the examples.

What is DWR?

In the simplest terms, DWR is an engine that exposes methods of server-side Java objects to JavaScript code. Effectively, with DWR, you can eliminate all of the machinery of the Ajax request-response cycle from your application code. This means your client-side code never has to deal with an XMLHttpRequest object directly, or with the server's response. You don't need to write object serialization code or use third-party tools to turn your objects into XML. You don't even need to write servlet code to mediate Ajax requests into calls on your Java domain objects.

DWR is deployed as a servlet within your Web application. Viewed as a black box, this servlet performs two major roles: First, for each exposed class, DWR dynamically generates JavaScript to include in your Web page. The generated JavaScript contains stub functions that represent the corresponding methods on the Java class and also performs XMLHttpRequests behind the scenes. These requests are sent to the DWR servlet, which, in its second role, translates the request into a method call on a server-side Java object and sends the method's return value back to the client side in its servlet response, encoded into JavaScript. DWR also provides JavaScript utility functions that help perform common UI tasks.



Back to top


About the example

Before explaining DWR in more detail, I'll introduce a simple example scenario. As in the previous articles, I'll use a minimal model based on an online store, this time consisting of a basic product representation, a user's shopping cart that can contain product items, and a data access object (DAO) to look up product details from a data store. The Item class is the same one used in the previous article, but it no longer implements any manual serialization methods. Figure 1 depicts this simple setup:


Figure 1. Class diagram depicting the Cart, CatalogDAO, and Item classes

I'll demonstrate two very simple use cases within this scenario. First, the user can perform a text search on the catalog and see matching items. Second, the user can add items to the shopping cart and see the total cost of items in the cart.



Back to top


Implementing the catalog

The starting point of a DWR application is writing your server-side object model. In this case, I start by writing a DAO to provide search capabilities on the product catalog datastore. CatalogDAO.java is a simple, stateless class with a zero-argument constructor. Listing 1 shows the signatures of the Java methods that I want to expose to Ajax clients:


Listing 1. The CatalogDAO methods to expose via DWR
/**

 * 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);


Next, I need to configure DWR, telling it that Ajax clients should be able to construct a CatalogDAO and call these methods. I do this with the dwr.xml config file shown in Listing 2:


Listing 2. Config to expose CatalogDAO methods
<!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>


The root element of the dwr.xml document is dwr. Inside this element is the allow element, which specifies the classes that DWR will remote. The two child elements of allow are create and convert.

The create element

The create element tells DWR that a server-side class should be exposed to Ajax requests and defines how DWR should obtain an instance of that class to remote. The creator attribute here is set to the value new, meaning that DWR should call the class's default constructor to obtain an instance. Other possibilities are to create an instance through a fragment of script using the Bean Scripting Framework (BSF), or to obtain an instance via integration with the IOC container, Spring. By default, when an Ajax request to DWR invokes a creator, the instantiated object is placed in page scope and therefore is no longer available after the request completes. In the case of the stateless CatalogDAO, this is fine.

The javascript attribute of create specifies the name by which the object will be accessible from JavaScript code. Nested within the create element, a param element specifies the Java class that the creator will create. Finally, include elements specify the names of the methods that should be exposed. Explicitly stating the methods to expose is good practice to avoid accidentally allowing access to potentially harmful functionality -- if this element is omitted, all of the class's methods will be exposed to remote calls. Alternately, you can use exclude elements to specify only those methods you wish to prevent access to.

The convert element

While creators are concerned with exposing classes and their methods for Web remoting, convertors are concerned with the parameters and return types of those methods. The role of the convert element is to tell DWR how to convert datatypes between their server-side Java object representation and serialized JavaScript representation, and vice versa.

DWR automatically mediates simple data types between Java and JavaScript representations. These types include Java primitives, along with their respective class representations, as well as Strings and Dates, arrays, and collection types. DWR can also convert JavaBeans into JavaScript representations, but for security reasons, doing so requires explicit configuration.

The convert element in Listing 2 tells DWR to use its reflection-based bean convertor for the Items returned by the exposed methods of CatalogDAO and specifies which of the Item's members should be included in the serialization. The members are specified using the JavaBean naming convention, so DWR will call the corresponding get methods. In this case, I'm omitting the numerical price field and instead including the formattedPrice field, which is currency-formatted ready for display.

At this point, I'm ready to deploy my dwr.xml to my Web application's WEB-INF directory, where the DWR servlet will pick it up. Before proceeding, however, it's a good idea to ensure that everything is working as expected.



Back to top


Testing the deployment

If the DWRServlet's web.xml definition sets the init-param debug to true, then DWR's extremely helpful test mode is enabled. Navigating to /{your-web-app}/dwr/ brings up a list of your classes that DWR has been configured to remote. Clicking through takes you to the status screen for a given class. The DWR test page for CatalogDAO is shown in Figure 2. As well as providing a script tag to paste into your Web pages, pointing to DWR's generated JavaScript for the class, this screen also provides a list of the class's methods. The list includes methods inherited from the class's supertypes, but only those methods that I explicitly specified for remoting in dwr.xml are marked as accessible.


Figure 2. DWR test page for CatalogDAO

It's possible to enter parameter values into the textboxes next to the accessible methods and hit the Execute button to invoke them. The server's response will be displayed using JSON notation in an alert box, unless it is a simple value, in which case it will be displayed inline alongside the method. These test pages are very useful. Not only do they allow you to easily check which classes and methods are exposed for remoting, you can also test that each method is behaving as expected.

Once you're satisfied that your remoted methods are working correctly, you can use DWR's generated JavaScript stubs to call on your server-side objects from client-side code.



Back to top


Calling a remoted object

The mapping between remoted Java object methods and their corresponding JavaScript stub functions is simple. The general form is JavaScriptName.methodName(methodParams ..., callBack), where JavaScriptName is whatever name was specified as the creator's javascript attribute, methodParams represents the Java method's n parameters, and callback is a JavaScript function that will be called with the Java method's return value. If you're familiar with Ajax, you'll recognize the callback mechanism as the usual approach to XMLHttpRequest's asynchrony.

In the example scenario, I use the JavaScript functions in Listing 3 to perform a search and update the UI with the search results. This listing also uses convenience functions from DWR's util.js. Of particular note is the JavaScript function named $(), which can be thought of as a souped-up version of document.getElementById(). It's certainly easier to type. If you've used the prototype JavaScript library, you'll be familiar with this function.


Listing 3. Calling the remoted findItems() method from the client
/*

 * 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";

  }

}

In the above searchFormSubmitHandler() function, the code of interest is of course catalog.findItems(searchexp, displayItems);. This single line of code is all that is needed to send an XMLHttpRequest over the network to the DWR servlet and call the displayItems() function with the remoted object's response.

The displayItems() callback itself is invoked with an array of Item representations. This array is passed to the DWRUtil.addRows() convenience function, along with the ID of a table to populate and an array of functions. There are as many functions in this array as there are cells in each table row. Each function is called in turn with an Item from the array and should return the content with which to populate the corresponding cell.

In this case, I want each row in the table of items to display the item's name, description, and price, as well as an Add to Cart button for the item in the last column. Listing 4 shows the cell functions array that accomplishes this:


Listing 4. Cell functions array to populate the items table
/*

 * 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;

  }

];


The first three of these functions simply return the content of the fields included in Item's convertor in dwr.xml. The last function creates a button, attaches the Item's ID to it, and specifies that a function named addToCartButtonHandler should be called when the button is clicked. This function is the entry point for the second use case: adding an Item to the shopping cart.



Back to top


Implementing the shopping cart

Security in DWR

DWR has been designed with security in mind. Using dwr.xml to whitelist only those classes and methods you wish to remote avoids accidental exposure of functionality that could be maliciously exploited. In addition to this, using the debug Test Mode, it is easy to audit all of the classes and methods that are exposed to the Web.

DWR also supports role-based security. You can specify the J2EE role that a user must be in to access a particular bean, via its creator configuration. By deploying multiple URL-secured instances of the DWRServlet, each with its own dwr.xml config file, you can also provide different sets of users with different remoted functionality.

The Java representation of the user's shopping cart is based on a Map. When an Item is added to the cart, the Item itself is inserted into the Map as the key. The corresponding value in the Map is an Integer representing the quantity of the given Item in the cart. Thus the Cart.java has a field, contents, declared as Map<Item,Integer>.

Using complex types as hash keys presents a problem to DWR -- in JavaScript, array keys must be literals. As a result, the contents Map cannot be converted by DWR as it is. However, for the purposes of the shopping cart UI, all the user needs to see is the name and quantity of each item in the cart. So I can add a method named getSimpleContents() to Cart, which takes the contents Map and builds a simplified Map<String,Integer> from it, representing only the name and quantity of each Item. This string-keyed map representation can simply be converted into JavaScript by DWR's built-in convertors.

The other field of Cart that the client is interested in is the totalPrice, representing the sum total of everything in the shopping cart. As with Item, I've provided a synthetic member named formattedTotalPrice that is a pre-formatted String representation of the numerical total.

Converting the Cart

Instead of having the client code make two calls on Cart, one to obtain the contents and one for the total price, I want to send all of this data to the client at once. To accomplish this I've added the strange-looking method, shown in Listing 5:


Listing 5. Cart.getCart() method
/**

 * Returns the cart itself - for DWR

 * @return the cart

 */ 

public Cart getCart() {

  return this;

}


While this method would be completely redundant in normal Java code (because you already have a reference to the Cart if you're calling the method), it allows a DWR client to have Cart serialize itself to JavaScript.

Aside from getCart(), the other method that needs to be remoted is addItemToCart(). This method takes a String representation of a catalog Item's ID, adds that item to the Cart, and updates the price total. The method also returns the Cart, so that the client code can update the Cart contents and receive its new state in a single operation.

Listing 6 is the extended dwr.xml config file that includes the extra config info for remoting the Cart class:


Listing 6. Modified dwr.xml incorporating the Cart class
<!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>

In this version of dwr.xml, I've added both a creator and a convertor for Cart. The create element specifies that the addItemToCart() and getCart() methods should be remoted and, importantly, that the created Cart instance should be placed in the user's session. As a result, the content of the cart will persist between user requests.

The convert element for Cart is necessary because the remoted Cart methods return the Cart itself. Here I've specified that the members of Cart that should be present in its serialized JavaScript form are the map simpleContents and the String formattedTotalPrice.

If you find this slightly confusing, remember that the create element specifies the server-side methods on Cart that can be called by a DWR client, and the convert element specifies the members to include in the JavaScript serialization of Cart.

Now I can implement the client-side code that calls my remoted Cart methods.



Back to top


Calling remoted Cart methods

First of all, when the store Web page first loads, I want to check the state of the Cart stored in session, if there is one. This is necessary because the user may have added items to the Cart and then refreshed the page or navigated away and then back again. In these circumstances, the reloaded page needs to sync itself with the Cart data in session. I can accomplish this with a call performed in the page's onload function, like so: Cart.getCart(displayCart). Note that displayCart() is a callback function invoked with the Cart response data from the server.

If a Cart is in session already, then the creator will retrieve it and call its getCart() method. If no Cart is in session, then the creator will instantiate a new one, place it in session, and call the getCart() method.

Listing 7 shows the implementation of the addToCartButtonHandler() function that is called when an item's Add to Cart button is clicked:


Listing 7. addToCartButtonHandler() implementation
/*

 * 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);

}


With DWR taking care of all the communication, the add-to-cart behavior on the client is literally a one-line function. Listing 8 shows the final piece of this jigsaw puzzle -- the implementation of the displayCart() callback, which updates the UI with the state of the Cart:


Listing 8. displayCart() implementation
/*

 * 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;

}


It's important to remember here that simpleContents is a JavaScript array mapping Strings to numbers. Each string is the name of an item, and the corresponding number in the associative array is the quantity of that item in the cart. So the expression cart.simpleContents[item] + " x " + item evaluates to, for example, "2 x Oolong 128MB CF Card".

The DWR Store application

Figure 3 shows my DWR-based Ajax application in action, displaying items retrieved by a search with the user's shopping cart on the right:


Figure 3. The DWR-based Ajax store application in action



Back to top


Pros and cons of DWR

Call batching

In DWR, several remote calls can be sent to the server with a single HTTP request. Calling DWREngine.beginBatch() tells DWR to not dispatch subsequent remoted calls straight away, but instead to combine them into a single batched request. A call to DWREngine.endBatch() sends the batched request to the server. The remote calls are executed in order on the server-side, and then each JavaScript callback is invoked.

Batching can help to reduce latency in two ways: first, you avoid the overhead of creating an XMLHttpRequest object and establishing the associated HTTP connection for each call. Second, in a production environment, the Web server won't have to deal with so many concurrent HTTP requests, improving response times.

You've seen how easy it is to implement a Java-backed Ajax application using DWR. Although the example scenario is a simple one and I've taken a fairly minimal approach to implementing the use cases, you shouldn't underestimate the amount of work the DWR engine can save you over a homegrown Ajax approach. Whereas in previous articles I walked through all the steps of manually setting up an Ajax request and response and converting a Java object graph into a JSON representation, here DWR has done all of that work for me. I wrote fewer than 50 lines of JavaScript to implement the client, and on the server-side, all I had to do was augment my regular JavaBeans with a couple of extra methods.

Of course, every technology has its drawbacks. As with any RPC mechanism, in DWR it can be easy to forget that each call you make on remoted objects is much more expensive than a local function call. DWR does a great job of hiding the Ajax machinery, but it's important to remember that the network is not transparent -- there is latency involved in making DWR calls, and your application should be architected so that remoted methods are coarse-grained. It is for this purpose that addItemToCart() returns the Cart itself. Although it would have been more natural to make addItemToCart() a void method, each DWR call to it would then have had to be followed with a call to getCart() to retrieve the modified Cart state.

DWR does have its own solution to the latency issue in call batching (see the sidebar Call batching). If you're unable to provide a suitably coarse-grained Ajax interface for your application, then use call batching wherever possible to combine multiple remote calls into a single HTTP request.

Separation of concerns

By its very nature, DWR creates a tight coupling between client-side and server-side code, with a number of implications. First, changes to the API of remoted methods need to be reflected in the JavaScript that calls on the DWR stubs. Second (and more significantly), this coupling causes client-side concerns to leak into server-side code. For instance, because not all Java types can be converted into JavaScript, it is sometimes necessary to add extra methods to Java objects so that they can be remoted more easily. In the example scenario, I resolved this by adding a getSimpleContents() method to Cart. I also added the getCart() method, which is useful in a DWR scenario but otherwise completely redundant. Given the need for a coarse-grained API on remoted objects and the problem of converting certain Java types to JavaScript, you can see how remoted JavaBeans can become polluted with methods that are useful only to an Ajax client.

To get around this, you might use wrapper classes to add extra DWR-specific methods to your plain-old JavaBeans. This would mean that Java clients of the JavaBean classes wouldn't see the extra fluff associated with remoting, and it would also allow you to give more friendly names to remoted methods -- getPrice() instead of getFormattedPrice(), for instance. Figure 4 shows a RemoteCart class that wraps Cart to add the extra DWR functionality:


Figure 4. RemoteCart wraps Cart for remoting functionality

Finally, you need to remember that DWR Ajax calls are asynchronous and should not be expected to return in the order they were dispatched. I ignored this small hurdle in example code, but in the first article of this series, I demonstrated how to timestamp responses as a simple safeguard against data arriving out-of-order.



Back to top


In conclusion

As you've seen, DWR has a lot to offer -- it allows you to create an Ajax interface to your server-side domain objects quickly and simply, without needing to write any servlet code, object serialization code, or client-side XMLHttpRequest code. It's extremely simple to deploy to your Web application using DWR, and DWR's security features can be integrated with a J2EE role-based authentication system. DWR isn't applicable to every application architecture, however, and it does require you to give some thought to the design of your domain objects' APIs.

If you want to learn more about the pros and cons of Ajax with DWR, the best thing would be to download it for yourself and start experimenting. While DWR has many features that I haven't covered, the article source code is a good starting point for taking DWR for a spin. See Resources to learn more about Ajax, DWR, and related technologies.

One of the most important points to take away from this series is that for Ajax applications, there is no one-size-fits-all solution. Ajax is a rapidly developing field with new technologies and techniques emerging all the time. In the three articles of this series, I've focused on getting you started with leveraging Java technologies in the Web tier of an Ajax application -- whether you choose an XMLHttpRequest-based approach with an object serialization framework or the higher level abstraction of DWR. Look out for further articles exploring Ajax for Java developers in the coming months.




Back to top


Download

Description Name Size Download method
DWR source code j-ajax3dwr.zip 301 KB FTP
Information about download methods

翻译:
下面是毕设中的翻译任务。原文是自己在网上找的资料,来自IBM。翻译的水平很烂。有些词觉得没法翻,找不到合适的中文对应。本质还是自己的英语学得不行啊。
 
 
Java 开发者的 Ajax技术 : 使用 Direct Web RemotingAjax
原文地址 : http://www-128.ibm.com/developerworks/java/library/j-ajax3/
没有比这更简单的数据串行化方法了!
水平:中等
Philip McCarthy ([email protected]),软件开发顾问,独立顾问
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来完成这个功能:
列表 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脚本框架(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显示了实现这个功能的单元函数数组:
列表 4. 放置 item 表格的单元函数数组
 
 
/*
 * 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显示的:
列表 5. Cart.getCart() 方法
 
 
/**
 * 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的状态更新用户界面:
列表 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是一个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

 
资源

你可能感兴趣的:(developer)