Retrofit existing applications with RMI

Minimize code changes and gain flexibility by using the Adapter pattern on the client side

Most tutorials on Java's Remote Method Invocation (RMI) technology use as examples new programs that were initially written to use RMI; that practice excludes those of us who modify existing Java code to use RMI. In this article, I'll show how applying the Adapter design pattern can make retrofitting your code much easier. I will build upon Dan Becker's excellent article, "Design Networked Applications in RMI Using the Adapter Design Pattern." (See Resources for a link.) Becker showed how to use the Adapter design pattern on the server side; I'll demonstrate its use on the client side.

If you are unfamiliar with RMI, I recommend jGuru's "Fundamentals of RMI" course. If you are unfamiliar with design patterns or the specifics of the Adapter pattern, I recommend Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, et al. Online tutorials are also available on the Patterns homepage. (See Resources for links to these materials.)

Why RMI?

RMI distributes an existing application's components across multiple systems. This is done for several reasons, including:

  • Portions of an application that once worked well on a single system have outgrown that system's processing capability
  • You need to move some parts of an application for security reasons
  • You wish to achieve reuse or concurrent use by multiple clients

Why the Adapter design pattern?

Initially, you must use RMI's protocol for making remote method calls. It would be beneficial, however, if most of your source code could keep using its existing protocol. The Adapter design pattern solves that problem by providing access to an object through a protocol that that object does not directly support. In this case, you should create adapter classes that wrap up the RMI protocol; this lets your existing code use a local protocol when making a method call on a remote object. Since an adapter class wraps up another object, it is frequently referred to as a wrapper class.

A simple example

The example application for this article has a class called StatisticsCalculator that performs statistical calculations. Given a Vector of numbers, StatisticsCalculator provides methods that calculate statistical values like standard deviation, median, mode, and mean. A snippet of the code is shown in Listing 1. (See Resources for the complete source code.)

public class StatisticsCalculator
{
  private Vector values_;
  public void setValues(Vector values)
  {
    values_ = values;
  }
  
  public double getStandardDeviation()
  {
    if ((values_ == null) || (values_.size() <= 1))
      return 0;
    .
    .
    .
   }
}

Listing 1. The StatisticsCalculator class

A typical usage of the StatisticsCalculator class is shown in Listing 2.

    StatisticsCalculator sc = new StatisticsCalculator();
    Vector v = new Vector();
    v.addElement(new Double(100));
    v.addElement(new Double(200));
    v.addElement(new Double(300));    
    sc.setValues(v);
    System.out.println("Std Dev: " + sc.getStandardDeviation());

Listing 2. Client usage of a local StatisticsCalculator

If you run the StatisticsCalculator object on another system, the easiest way to access it on that remote system is with RMI. By following the standard steps for RMI support, you can create an interface that extends java.rmi.Remote. Let's call it RemoteStatisticsCalculatorService; it is shown in Listing 3.

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Vector;
public interface RemoteStatisticsCalculatorService extends Remote
{
  public static final String SERVICENAME = 
                  "RemoteStatisticsCalculatorService";
  
  public void setValues(Vector values) throws RemoteException;
  public double getStandardDeviation() throws RemoteException;
}

Listing 3. The RemoteStatisticsCalculatorService interface

At this point, some people would modify the StatisticsCalculator class to directly implement the RemoteStatisticsCalculatorService. Having read Becker's article, though, I will implement the RemoteStatisticsCalculatorService interface with a wrapper class that provides remote access to a StatisticsCalculator object. The result, RemoteStatisticsCalculatorServer, is shown in Listing 4. It includes a main() method that creates the object being wrapped and passes it to the constructor.

import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Enumeration;
import java.util.Vector;
public class RemoteStatisticsCalculatorServer 
       extends UnicastRemoteObject 
       implements RemoteStatisticsCalculatorService
{
  // object being wrapped
  private StatisticsCalculator calc_;
  public RemoteStatisticsCalculatorServer(StatisticsCalculator calc) 
    throws RemoteException
  {
    calc_ = calc;
  }
  
  public void setValues(Vector values) throws RemoteException
  {
    calc_.setValues(values);
  }
  
  public double getStandardDeviation() throws RemoteException
  {
    return calc_.getStandardDeviation();
  }
  
  public static void main(String[] args)
  {
    try
    {
      // start the registry
      Registry registry = LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
    
      // create a StatisticsCalculator, and then bind our 
      // server object to the service
      // name RemoteStatisticsCalculatorServer
      registry.rebind(RemoteStatisticsCalculatorService.SERVICENAME, 
        new RemoteStatisticsCalculatorServer(new StatisticsCalculator()));
    }
    catch (RemoteException e)
    {
      System.out.println(e);
    }
  }
}

Listing 4. The StatisticsCalculator server program

With the RMI server program complete, you can finally change the code that uses a local StatisticsCalculator object. This program will now be an RMI client that attempts to establish a remote connection with an object that implements the RemoteStatisticsCalculatorService. This client's code is shown in Listing 5.

    try
    {
      // find the registry
      Registry remoteRegistry = LocateRegistry.getRegistry(rmiHostName_);
      
      // get a reference to the remote calculator
      RemoteStatisticsCalculatorService sc =  
         (RemoteStatisticsCalculatorService) 
             remoteRegistry.lookup(RemoteStatisticsCalculatorService.SERVICENAME);
      Vector v = new Vector();
      v.addElement(new Double(100));
      v.addElement(new Double(200));
      v.addElement(new Double(300));    
      sc.setValues(v);
      System.out.println("Std Dev: " + sc.getStandardDeviation());
    }
    catch (NotBoundException e)
    {
      System.out.println(e);
    }
    catch (RemoteException e)
    {
      System.out.println(e);
    }

Listing 5. Client usage of a remote StatisticsCalculator

Much has changed between Listing 2 and Listing 5. In addition to adding a call to remoteRegistry.lookup(), the RMI protocol requires adding try/catch blocks for the RMI exceptions that can be thrown. So everywhere you use StatisticsCalculator in the existing code, you must add this remote exception handling. This can be a considerable burden, which you can avoid by using the Adapter pattern for the RMI client code. The first step is to define a second interface that does not extend java.rmi.Remote. I'll call it StatisticsCalculatorService; it is shown in Listing 6.

import java.util.Vector;
public interface StatisticsCalculatorService
{
  public void setValues(Vector values);
  public double getStandardDeviation();
}

Listing 6. The StatisticsCalculatorService interface

RMI is not mentioned in the StatisticsCalculatorService interface -- it just provides an abstraction of the functionality in the original StatisticsCalculator class. Now you can define an Adapter class for the client that wraps the RMI functionality for you. I'll call it StatisticsCalculatorAccess, since it provides access to a StatisticsCalculatorService.

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Vector;
public class StatisticsCalculatorAccess implements StatisticsCalculatorService
{
  private String rmiHostName_ = "localhost";
  // the remote calculator
  private RemoteStatisticsCalculatorService remoteCalculator_;
  
  public StatisticsCalculatorAccess()
  {
    try
    {
      // find the registry
      Registry remoteRegistry = LocateRegistry.getRegistry(rmiHostName_);
      
      // get a reference to the remote calculator
      remoteCalculator_ = 
         (RemoteStatisticsCalculatorService) 
            remoteRegistry.lookup(RemoteStatisticsCalculatorService.SERVICENAME);
    }
    catch (NotBoundException e)
    {
      System.out.println(e);
    }
    catch (RemoteException e)
    {
      System.out.println(e);
    }
  }
  
  public void setValues(Vector values)
  {
    try
    {
      remoteCalculator_.setValues(values);
    }
    catch (RemoteException e)
    {
      System.out.println(e);
    }
  }
  
  public double getStandardDeviation()
  {
    double retVal = 0;
    try
    {
      retVal = remoteCalculator_.getStandardDeviation();
    }
    catch (RemoteException e)
    {
      System.out.println(e);
    }
    return retVal;
  }
}

Listing 7. An adapter for local access to a remote StatisticsCalculator

To modify your existing code to use StatisticsCalculatorAccess, you must change only one line, as shown in Listing 8.

    StatisticsCalculatorService sc = new StatisticsCalculatorAccess();
    Vector v = new Vector();
    v.addElement(new Double(100));
    v.addElement(new Double(200));
    v.addElement(new Double(300));    
    sc.setValues(v);
    System.out.println("Std Dev: " + sc.getStandardDeviation());

Listing 8. Client usage of the StatisticsCalculatorAccess adapter

If you compare Listing 8 with Listing 2, you will see that only the first line is different, because the StatisticsCalculatorAccess class is handling the RMI details. This lets you partition the application with RMI while minimizing the impact on your existing code. Also, placing the RMI code in a wrapper class will result in a smaller impact if you eventually stop using RMI.

You can even take the idea a couple of steps further. Suppose that under some circumstances, you don't need a remote StatisticsCalculator: for example, the list of numbers is so small that sending them across the network for a remote-system computation isn't worth the overhead. You can modify the StatisticsCalculatorAccess class so its constructor knows whether to create a local StatisticsCalculatorService or get access to a remote one. This is shown in Listing 9.

public class StatisticsCalculatorAccess implements StatisticsCalculatorService
{
  private String rmiHostName_ = "localhost";
  // the remote calculator
  private RemoteStatisticsCalculatorService remoteCalculator_;
  / the local calculator
  private StatisticsCalculatorService localCalculator_;  
  // state flag to indicate if we're wrapping a
  // local or remote calculator
  private boolean remote_;
  
  public StatisticsCalculatorAccess()
  {
    this(true);
  }
    
  public StatisticsCalculatorAccess(boolean remote)
  {
    remote_ = remote;
    
    if (remote)
    {
      // get access to a remote calculator
      try
      {
        // find the registry
        Registry remoteRegistry = LocateRegistry.getRegistry(rmiHostName_);
        
        // get a reference to the remote calculator
        remoteCalculator_ = (RemoteStatisticsCalculatorService) 
               remoteRegistry.lookup(RemoteStatisticsCalculatorService.SERVICENAME);
      }
      catch (NotBoundException e)
      {
        System.out.println(e);
      }
      catch (RemoteException e)
      {
        System.out.println(e);
      }
    }
    else
    {
      // just create a local calculator
      localCalculator_ = new StatisticsCalculator();
    }
  }
  
  public void setValues(Vector values)
  {
    if (remote_)
    {
      try
      {
        remoteCalculator_.setValues(values);
      }
      catch (RemoteException e)
      {
        System.out.println(e);
      }
    }
    else
    {
      localCalculator_.setValues(values);
    }
  }
  
  public double getStandardDeviation()
  {
    double retVal = 0;
    
    if (remote_)
    {
      try
      {
        retVal = remoteCalculator_.getStandardDeviation();
      }
      catch (RemoteException e)
      {
        System.out.println(e);
      }
    }
    else
    {
      retVal = localCalculator_.getStandardDeviation();
    }
    return retVal;
  }
}

Listing 9. A more flexible StatisticsCalculatorAccess adapter

The no-argument constructor will create a StatisticsCalculatorAccess object that is using a remote object. That means the code in Listing 8 remains unchanged if that is the desired behavior. To use a local object, you can change the first line in Listing 8 to:

StatisticsCalculatorService sc = new StatisticsCalculatorAccess(false);

Countless other possibilities exist: the constructor could decide whether to use a local or remote object based on a remote object's availability, or even on system load. By handling the details in the StatisticsCalculatorAccess class, you make your program flexible with minimal impact on existing source code.

1 2 Page 1
Page 1 of 2