Design networked applications in RMI using the Adapter design pattern

A guide to correctly adapting local Java objects for the Web

If you use Java's Remote Method Invocation (RMI), you know that it's easy to understand and implement: The perfect recipe for creating remote objects and services. In fact, Sun's Java tutorial provides an excellent example for creating your first RMI server and client and for running a remote system. However, I've seen many designs that build upon the Sun example and become unwieldy as new functions are added. Novice programmers often resort to odd naming conventions to distinguish local behavior from remote behavior, and, with the addition of new remote services, the remote class can easily grow into a big mess that performs poorly under remote network traffic.

In this article I'll demonstrate how the Adapter pattern, one of the most common design patterns, can be used to adapt a local class to a remote class. Using this design technique, it's easy to distinguish local from remote behavior, and you'll be able to continue to use the local class for your non-networked applications. Also, as the number of remote services grow, it's easy to see which class must handle the request and which objects must be synchronized. Finally, with this technique, your design will be easier to understand and more amenable to changes and fixes from multiple developers.

What is the Adapter pattern?

The Adapter pattern is one of the structural patterns listed in the reference book Design Patterns by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). The Adapter pattern is very common not only to remote client/server programming but also to any situation in which you have one class that you wish to reuse, but the application interface doesn't match the class interface. For this reason, you use an adapter or wrapper to convert the application interface to the existing class interface. In software development, an adapter simply maps the interface of one class to that of another. Adapters are used continually throughout the software development process, hence the term Adapter pattern.

Figure 1, below, illustrates how an adapter works. In this diagram, Client wants to invoke the method request() in the Target interface. Since the Adaptee class has no request() method, it is the job of Adapter to convert the request to an available matching method. Here the Adapter converts the method request() call into the Adaptee method specificRequest() call. The Adapter does this conversion for each method that needs adapting, also known as wrappering.

Figure 1. Adapting an interface to an Adaptee class

This type of adapting is exactly what one needs when creating remote services for the Web. Think of the design process as taking a working local class and publishing its services for the Web. You publish the services not by adding to the local class, but by adapting the local class to the remote interface. In the following sections I'll show you how this is done.

Designing a networked application

As stated in the previous section, classes are best made Web-ready by adapting the local methods to a remote interface. Sun's Java tutorial tells us we have to follow certain steps to make RMI work. These steps are as follows:

  1. Create a remote interface that extends the java.rmi.Remote interface

  2. Provide an implementation of the remote interface

  3. Invoke the remote method calls from the remote client

Serendipitously, these steps closely match the function of the Adapter pattern. The Adapter pattern is initiated when a remote client intends to use a method published in the remote interface. The remote interface extends the tagging interface java.rmi.Remote to let RMI know this interface implements RMI. Rather than forcing the local class to implement the remote method, which creates excessive class bloating and hurts performance and readability, one creates a remote adapter that services a remote method by invoking a similar method in the local class. The local class is unchanged and unaware that it's being adapted to service network requests. Figure 2 shows the relationships of the classes as you apply the Adapter pattern to RMI.

Figure 2. Adapting a Remote interface to a local class

Conceptually, we've created a picture of how we want to use the Adapter pattern in a Java RMI application. Now let's create a concrete implementation of this design. Let's assume we want to implement a remote collection that many remote clients can query or add and delete elements from over a network. For this example, I'll use the Hashtable class in the java.util package, which many Java programmers have used over and over again.

Design the local classes

The first step toward creating a remote collection is to design a local class that implements some function. Conveniently, Sun has created the java.util.Hashtable for us, and we'll use the class as-is with no extra function. I've omitted the synchronized keyword from the methods, but I will reintroduce it later when I discuss how to make the object thread-safe. For your convenience, Listing 1 contains the pertinent methods of the Hashtable class.

int size();
boolean isEmpty();
boolean contains( Object value );
boolean containsKey( Object key );
Object get( Object key );
Object put( Object key, Object value );
Object remove( Object key );
void clear();
Enumeration keys();
Enumeration elements();

Listing 1. The java.util.Hashtable class methods

Design the remote interface

The second step in this exercise is to create a remote interface that takes the services the remote class is offering and publishes them over the network. The interface RemoteMap, shown below, is based on the java.util.Map collection interface, which the hashtable implements in Java 2. Collectively, these methods form the interface that remote clients may call on our networked hashtable.

public interface RemoteMap extends java.rmi.Remote {
   // Constants
   public static final String SERVICENAME = "RemoteMap";
   public int size() throws RemoteException;
   public boolean isEmpty() throws RemoteException;
   public boolean containsKey( Object key ) throws RemoteException;
   public boolean containsValue( Object value ) throws RemoteException;
   public Object get( Object key ) throws RemoteException;
   public Object put( Object key, Object value ) throws RemoteException;
   public Object remove( Object key ) throws RemoteException;
   public void clear() throws RemoteException;
}

Listing 2. A RemoteMap interface: Our remote methods

There are a few details worth pointing out. To start with, all of the remote methods throw a java.rmi.RemoteException. This is nothing to worry about; it's simply a requirement of Java RMI. Also, the RemoteMap interface extends the java.rmi.Remote interface. This is a tagging interface that tells the Java rmic utility which interface is going to be shared over the network; this is another requirement of RMI. I've left out a few of the Map interface methods, such as keySet and valueSet, but these are easy to implement using the hashtable keys and elements enumerations. Finally, there is one constant in the interface -- the string SERVICENAME. This constant will be handy when servers publish an RMI service and clients query the RMI registry to find the name of our public services.

So, now we have a local object and a remote interface. We're now ready to adapt the local class to the remote interface.

Pitfalls of adaptation

Many designers fail to properly execute the next step, adapting the local class to the remote interface. It's tempting to create a new class that extends the java.util.Hashtable class and implements the RemoteMap interface (for example, RemoteHashtable). This is a bad programming choice for several reasons:

  • It's preferable to design using composition rather than inheritance. Why? Because design by inheritance is normally used for adding new methods and attributes to an existing class. Here, we're simply adapting local methods to become remote methods, so design by composition and delegation is more appropriate.

  • Designing with inheritance uses up the single inheritance parent class. Since Java only allows single inheritance, this gives us the dubious choice of extending java.rmi.server.UnicastRemoteObject, as required by RMI, or extending java.util.Hashtable, as required when designing by inheritance. It's preferable to forgo design by inheritance and implement the RMI function.

  • Some local classes are final and may not be extended. For example, this design exercise won't work with the java.util.Vector class because that class is final. If a class is final, you cannot use design by inheritance.

  • Mixing local and remote functions overly complicates the class. This is especially problematic when the remote interface and the local class have similar method names. I've seen supposed solutions where programmers have changed the method names to help prevent this problem -- for example, sizeLocal and sizeRemote. This is ugly! Not to mention that two simple, single-function classes now become one big multifunction class. Even uglier!

To show you how easy it is to create an adapter, and how simple is its function, Listing 3 offers the entire code. Java RMI handles the marshalling and unmarshalling of all call parameters and return values as long as these objects are serializable and implement java.io.Serializable. So, you see, the adapter really is a very simple class, and we haven't made one change to our local implementation, the hashtable.

public class RemoteMapAdapter extends UnicastRemoteObject
   implements RemoteMap {
   // Constructors
   // The owner publishes this service in the RMI registry.
   public RemoteMapAdapter( Hashtable adaptee ) throws RemoteException {
      this.adaptee = adaptee;
   }
   // RemoteMap role
   public int size() throws RemoteException {
      return adaptee.size(); }
   public boolean isEmpty() throws RemoteException {
      return adaptee.isEmpty(); }
   public boolean containsKey( Object key ) throws RemoteException {
      return adaptee.containsKey( key ); }
   public boolean containsValue( Object value ) throws RemoteException {
      return adaptee.contains( value ); }
   public Object get( Object key ) throws RemoteException {
      return adaptee.get( key ); }
   public Object put( Object key, Object value ) throws RemoteException {
      return adaptee.put( key, value ); }
   public Object remove( Object key ) throws RemoteException {
      return adaptee.remove( key ); }
   public void clear() throws RemoteException {
      adaptee.clear(); }
   // Fields
   protected Hashtable adaptee;
}

Listing 3. A RemoteMapAdapter, adapting RemoteMap to Hashtable

We've finished the most important part of this example -- we've finished writing our adapter class. Let's wrap up a few more loose ends, and we'll be done.

Final touches

To finish the exercise, we must do a few more things. First, we must compile the interface and the adapter given in the previous two sections. Second, we must create the stubs and skeletons required to run the RemoteMapAdapter over a network. This is done in Java using the rmic tool. To run rmic on the RemoteMapAdapter class, type: rmic RemoteMapAdapter.

Next, we need two simple programs to run this example -- a server and a client. First, we implement a remote server that creates a local hashtable and publishes the remote version using the adapter we've designed. The important bits are shown below. Unlike many examples I've seen, it's possible to run a Java RMI server from within a program and not from the command line. The createRegistry method performs this step. Then, we create both the local hashtable and the adapter that performs the remote publishing work. Finally, the RMI registry publishes the RemoteMapAdapter using a string name and the rebind method of the local registry. Voila, the server is running and ready to handle remote clients! See Listing 4.

   ...
   try {
      System.out.println( "RemoteMapServer creating a local RMI registry on
 the default port." );
      Registry localRegistry = LocateRegistry.createRegistry(
Registry.REGISTRY_PORT );
      System.out.println( "RemoteMapServer creating local object and remote
 adapter." );
      adaptee = new Hashtable();
      adapter = new RemoteMapAdapter( adaptee );
      System.out.println( "RemoteMapServer publishing service \"" +
RemoteMap.SERVICENAME + "\" in local registry." );
      localRegistry.rebind( RemoteMap.SERVICENAME, adapter );
      System.out.println( "Published RemoteMap as service \"" +
RemoteMap.SERVICENAME + "\". Ready." );
   } catch (RemoteException e) {
      System.out.println( "RemoteMapServer problem with remote object,
exception:\n   " + e );
   }
   ...

Listing 4. A RemoteMapServer that publishes a RemoteMapAdapter

The last step is to create a few clients that can add, query, and remove objects remotely. Listing 5 shows how the client finds the remote registry on the server and queries the service offered via the given service name. Notice that the registry lookup method returns the RemoteMap interface and not a RemoteMapAdapter object. This is true of all RMI services because many types of remote objects may implement the remote interface.

   ...
   try {
      System.out.println( "RemoteMapClient locating RMI registry on remote
host \"" + name + "\"." );
      Registry remoteRegistry = LocateRegistry.getRegistry( name );
      System.out.println( "RemoteMapClient looking up service \"" +
RemoteMap.SERVICENAME + "\"." );
      remoteMap = (RemoteMap) remoteRegistry.lookup( RemoteMap.SERVICENAME
);
   } catch (Exception e) { // expecting RemoteException, NotBoundException
      System.out.println( "RemoteMapClient problem with RemoteMap,
exception:\n   " + e );
   }
   ...

Listing 5. Finding the remote registry and a remote service

Once we have a reference to a RemoteMap, we can invoke any of the methods in its interface. Java RMI makes this easy by marshalling and unmarshalling all parameters and return values. In fact, remote method calls look exactly the same as local method calls. Just beware: unlike local calls, remote calls may be delayed by traffic and problems on the Internet. The delivery of the information is guaranteed, but ideally these calls should be on a separate thread to ensure the local Java application paints correctly and behaves in a responsive manner.

Listing 6 shows how to call the remote object.

   ...
   try {
      Object value = remoteMap.get( name );
      if (value != null )
         System.out.println( "RemoteMapClient found " + name + ", value=" +
 value );
      else
         System.out.println( "RemoteMapClient could not find key " + name +
 "." );
   } catch (RemoteException e) {
      System.out.println( "RemoteMapClient problem with " + name + ",
exception:\n   " + e );
   } /* endcatch */
   ...

Listing 6. Invoking methods on the remote object

To use this client/server system, first start up the server. The server instantiates a local hashtable and a remote adapter, and places objects in the hashtable. The hashtable is published in the RMI registry on the local machine. For example, from a command line on Windows, Unix, or Operating System/2, type:

java RemoteMapServer Harriet Bailey Max Zuzu

Now start a client to query and instantiate the contents of the remote object. If you don't have a network, you may substitute the TCP/IP localhost name or the IP address 127.0.0.1 in the commands below, although some systems require that loopback is enabled or localhost is listed in the etc/hosts file. Using your IP address, your machine name, or the localhost name, execute this command:

java RemoteMapClient hostName Zuzu

If your Java environment is installed and running properly you should see the remote reply:

RemoteMapClient found Zuzu, value=3

Time to take a breather! You've come a long way in understanding RMI and the Adapter design pattern. Take some time to relax and enjoy the fruits of your labor. I suggest you take the client and server code and play with it. Modify it for your use. If possible, run the program on several different machines or several different Java virtual machines. You should find that the program acts similarly on dissimilar platforms.

Conclusion

After reading this article, you should recognize the value of using the Adapter pattern in a Java RMI program. The advantages are that you don't change your local object, and you don't create excessively heavy or low-performance remote objects. By separating responsibilities along class lines, you have a design that is easy to debug: problems with the local objects belong to the local adaptee class; problems with the network belong to the remote adapter class. In addition to these advantages, once you've created one adapter, you can reuse the pattern over and over again -- that's the value of design patterns. The implementation is quite easy to do and easy to repeat, and it's easily read by people inspecting or maintaining your code.

Dan works in the Network Computing Software Division of IBM Corporation in Austin, Texas. He is currently porting IBM's implementation of Java 2 to the AIX, Operating System/2, System 390, and Windows platforms. Before that Dan worked on porting previous Java virtual machines, the multimedia plugins for Netscape Navigator for OS/2, OpenDoc and the Multimedia parts for OS/2 Warp Version 4.0. Dan's public Web page is http://www.io.com/~beckerdo.

Learn more about this topic

  • Sun's Java tutorial on RMI -- an excellent starting place for RMI beginners http://java.sun.com/docs/books/tutorial/rmi/TOC.html
  • Sun's page on the Java Collections interfaces in Java 2 http://java.sun.com/products/jdk/1.2/docs/guide/collections/index.html
  • Design PatternsElements of Reusable Object-Oriented Software Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Addison Wesley, 1994) A solid overview of design patterns http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0201633612
  • Patterns in Java, Volume 1, Mark Grand (John Wiley & Sons, 1998) A good resource for learning about patterns with specific implementations in Java http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0471258393
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more