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.
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 XMLHttpRequest
s 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.
|
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:
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.
|
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:
/** * 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:
<!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 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.
While creator
s are concerned with exposing classes and their methods for Web remoting, convertor
s 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 Item
s 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.
|
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.
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.
|
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.
/* * 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:
/* * 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.
|
Implementing the shopping cart
|
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.
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:
/** * 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:
<!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.
|
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:
/* * 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
:
/* * 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 String
s 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
".
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:
|
|
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.
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:
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.
|
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.
|
Description | Name | Size | Download method |
---|---|---|---|
DWR source code | j-ajax3dwr.zip | 301 KB | FTP |
Information about download methods |
/**
* 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);
|
<!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>
|
/*
* 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";
}
}
|
/*
* 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
的安全性
DWR在设计时就考虑了安全问题。在dwr.xml中只把把想暴露给远程的类和方法加入到白名单中,这样避免了不经意地暴露可能被策划用来攻击的功能。另外,在调试测试模式下,很容易就能审核暴露给Web的所有类和方法。
DWR还支持基于角色的安全机制。通过bean的 creator配置,可以指定只有某个J2EE角色才能访问这个bean。通过部署多个安全DWR Servlet实例的多个URL(每个Servlet都有自己的dwr.xml配置文件),你可以定义有不同远程功能调用的用户集合。
|
/**
* Returns the cart itself - for DWR
* @return the cart
*/
public Cart getCart() {
return this;
}
|
<!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>
|
/*
* 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);
}
|
/*
* 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;
}
|
批量调用
在DWR中可以把几个远程调用放在单个的HTTP请求中发送给服务端。调用 DWREngine.beginBatch()告诉DWR不要把后面的远程调用直接发出去,相反要把它们组合进单个的批请求中。调用 DWREngine.endBatch()把批调用发给服务端。远程调用在服务端按顺序调用,并且每个JavaScript回调函数都被调用。
批处理可以在两方面减少延迟:第一,你可以避免创建 XMLHttpRequest对象和为每个调用建立HTTP连接的开销。第二,在一个产品环境中,Web服务器不需要同时处理这么多HTTP请求,提高了响应次数。
|
Description | Name | Size | Download method |
---|---|---|---|
DWR source code | j-ajax3dwr.zip | 301 KB | FTP |