Empower RMI with TRMI

Transparent RMI simplifies distributed application development

The Java RMI (Remote Method Invocation) API provides us with a clean way to build distributed Java applications. The components that make up these applications communicate with each other by invoking methods on their remote counterparts. Transparent RMI (TRMI) extends the RMI API to simplify distributed application development by eliminating most of the standard API's overhead.

This article details the benefits of using TRMI in place of standard RMI. It shows how distributed software developers can provide a cleaner design for their applications and focus on the problem at hand rather than on the details of remote invocation. The article also discusses how TRMI allows developers to easily retrofit an existing application with remote objects. I assume that you are familiar with the basics of RMI and Java's proxy mechanism.

Before describing RMI's drawbacks, let's review the steps required for creating a distributed application using RMI:

  1. Create an interface that will be called remotely. This interface must extend java.rmi.Remote. Furthermore, its methods must all declare that they throw java.rmi.RemoteException in case of an invocation problem.
  2. Create an implementation class that implements this interface. The implementation class should extend java.rmi.server.UnicastRemoteObject, which implements all the setup required of an RMI server object.
  3. Create a server program that initializes the implementation object and binds it to a unique URL using java.rmi.Naming.
  4. Clients wanting to use the remote instance look it up using that URL and can then call its methods. A method call is marshaled and passed over the network to the remote object, which unmarshals it, executes it locally, and returns the result (or propagates the exception, as the case may be) to the caller.

A developer wishing to create a nontrivial application using RMI faces several difficulties:

  • Because of the requirements imposed on remote interfaces (they must implement Remote, and their methods must throw RemoteException), interfaces not designed with RMI in mind cannot be used remotely.
  • Calls to remote interfaces prove cumbersome, as they must be enclosed in a try/catch block for catching the possible RemoteException. Therefore, you must scatter exception-handling code throughout the application. To avoid doing so, developers usually limit remote invocation to a small portion of their programs.
  • The nuisance of RemoteExceptions also makes it difficult to use interfaces designed for remote execution locally.
  • No convenient approach can generically handle disconnections from a server in a single location. (An example of such handling might include looking up the remote object again and trying to reinvoke the method.)
  • Implementations of Remote interfaces cannot easily extend arbitrary classes, since they normally extend UnicastRemoteObject. (You could avoid this limitation with some effort, by reimplementing UnicastRemoteObject.)

Enter transparent RMI

Transparent RMI overcomes these difficulties by using Java's reflection and proxy mechanisms. Before delving into TRMI's details, let's see what a program that uses TRMI looks like. (Note that the code presented here is a slightly modified version of the code you can download from Resources.) Suppose we want to call the Hello interface from a remote JVM:

public interface Hello {
   /**
    * Returns a hello string
    */
   public String hello();
}

Hello's implementation is trivial:

 public class HelloImpl implements Hello {
   public String hello() {
      System.out.println("hello() called");
      return "Hi there!";
   }
}

To access the object remotely, we set up a HelloServer:

 import trmi.*;
public class HelloServer {
   public static void main(String[] args) {
      String name;
      // ...
        try {
         // Create the object --
         Hello hello = new HelloImpl();
         // -- and bind it
         trmi.Naming.rebind(
            name, 
            hello, 
            new Class[] {Hello.class});
        } 
      
      catch (Exception e) {
         e.printStackTrace();
         System.exit(1);
      }
      // ...
   }
}

Note how the program above binds the remote object: We use the trmi.Naming class, which has semantics similar to java.rmi.Naming's, albeit with a few important differences. Most notably, trmi.Naming deals in everyday objects and interfaces, while java.rmi.Naming handles Remote instances.

The HelloImpl instance is bound more or less as a typical remote object (using rebind()), with an important difference: HelloImpl implements the simple Hello interface, which isn't a Remote interface. We also tell trmi.Naming which interfaces we want the object to expose remotely. We must do this because, unlike java.rmi.Naming, trmi.Naming cannot easily differentiate between remote and nonremote interfaces.

We now have a server that has a remotely exposed Hello implementation. We use it like this:

 public class HelloClient {
   public static void main(String[] args) {
      String name;
      Hello hello;
      // ...
      try {
         // Look up the object
         hello = (Hello) trmi.Naming.lookup(name);
      } catch (Exception e) {
         e.printStackTrace();
         System.exit(1);
      }
      System.out.println("Saying hello...");
      // Make the call (look Ma, no try block!)
      String response = hello.hello();
      System.out.println(
         "Hello server replied: " 
         + response + "\n");
   }
}

We first look up the object using trmi.Naming.lookup, this time exactly as we look up a standard RMI object. We then invoke the hello() method on the object we looked up, treating the object as if it were local; no try/catch block surrounds the call, and we don't perform any error handling. Those tasks happen behind the scenes, as you shall see.

The gory details: Client side

So, how does TRMI work? As stated above, TRMI uses Java's proxy mechanism to perform its magic. The mechanism encapsulates actual RMI invocation in a pair of classes—StubInvocationHandler and RemoteObjectWrapperImpl—that act as a bridge between any interface and its (remote) implementation.

When trmi.Naming.bind binds an object on the server, a RemoteObjectWrapperImpl instance is created to wrap it. This is the instance that java.rmi.Naming actually binds to the RMI registry. The instance exposes two remote methods declared in RemoteObjectWrapper, a standard RMI Remote interface. The more important method of the two is invokeRemote(), which the client calls to invoke a method on the wrapped object. More on that later.

Here is how the object is bound:

 package trmi;
public class Naming {
   // ...
   public static void bind(String name, Object obj, Class[] ifaces) 
   throws AlreadyBoundException, ...  {
      // Create the wrapper
      RemoteObjectWrapper wrapper = 
         new RemoteObjectWrapperImpl(obj, ifaces);
      // Bind the wrapper
      java.rmi.Naming.bind(name, wrapper);
   }
}

Note that the RemoteObjectWrapperImpl constructor accepts both the wrapped object and the interfaces to be exposed remotely. As mentioned earlier, the user must let TRMI know which interfaces to expose and which to keep local because those details aren't clearly indicated as they are with RMI (that is, using extends Remote). You should also note that standard RMI is used to bind the remote wrapper to the RMI registry. Similarly, all the naming-related methods (unbind() and lookup(), for example) in trmi.Naming use standard RMI; doing so ensures that TRMI will be compatible with future versions of RMI and JNDI (Java Naming and Directory Interface).

On the client side, trmi.Naming.lookup() looks up a bound remote object:

 public class Naming {
   public static Object lookup(String name) throws NotBoundException, ... {
      Remote remoteObj = java.rmi.Naming.lookup(name);
        
      // If this is a transparent-RMI object, handle it accordingly
      if (remoteObj instanceof RemoteObjectWrapper) {
         RemoteObjectWrapper wrapper = (RemoteObjectWrapper) remoteObj;
         return createStub(name, wrapper);
      }
      // Otherwise, return the standard RMI stub
      else {
         return remoteObj;
      }
   }
   private static Object createStub(
         String name, 
         RemoteObjectWrapper wrapper) throws RemoteException {
      Class[] exposedInterfaces = wrapper.exposedInterfaces();
      // Create the invocation handler
      RemoteExceptionRecoveryStrategy recoveryStrategy;
      if (name != null) {
         recoveryStrategy = recoveryStrategyFactory.getRecoveryStrategy(
               name, exposedInterfaces);
      } else {
         recoveryStrategy = recoveryStrategyFactory.getRecoveryStrategy(
               exposedInterfaces);
      }
      StubInvocationHandler invocationHandler = new StubInvocationHandler(
            wrapper, recoveryStrategy);
      // Create a proxy that supports the object's exposed interfaces
      Object stub = Proxy.newProxyInstance(
            trmi.Naming.class.getClassLoader(),
            exposedInterfaces,
            invocationHandler);
      return stub;      
   }
}

After trmi.Naming retrieves the object with java.rmi.Naming, it checks to see if the object is indeed a TRMI wrapper. If it isn't, then the object must be a standard RMI remote object, and returns as-is. If the object is a TRMI wrapper, a TRMI stub is created for it in createStub(). The stub is a proxy implementation of all the object's exposed interfaces, with a StubInvocationHandler at its core. Note the reference to a recoveryStrategyFactory: the centralized error handling takes place there, as you'll see later. For now, let's see what happens when the client calls hello() on its proxy stub. The figure below shows the TRMI setup required to execute the call.

Typical TRMI setup while invoking hello()

The call first reaches the stub's StubInvocationHandler:

 public class StubInvocationHandler 
implements InvocationHandler, Serializable {
   // ...
   public Object invoke(
         Object proxy, 
         Method method, 
         Object[] params) 
      throws Throwable, RuntimeException {
      // Loops while we fail to make the call
      while (true) {
         try {
            // Handle the primitives issue
            Class[] convertedTypes = 
               method.getParameterTypes();
            boolean[] primitiveTypes = 
               new boolean[convertedTypes.length];
            convertPrimitiveParamTypes(
                  convertedTypes,
                  primitiveTypes);
            // Convert non-Serializable parameters to TRMI stubs
            convertNonSerializableParams(convertedTypes, params);
            // Tell the wrapper to invoke the method
            Object response = wrapper.invokeRemote(
                  method.getName(),
                  convertedTypes,
                  primitiveTypes,
                  params);
            return response;
         } catch (RemoteObjectWrapperException e) {
            // This indicates a bug in this suite
            throw new RuntimeException(
                  "Internal trmi error while invoking "
                  + method + ": " + e);
         } catch (InvocationTargetException e) {
            // The invoked method raised an exception
            throw e.getCause();
         } catch (MarshalException e) {
            // We can't recover from this type of exception
            throw new RuntimeException(e);
         } catch (RemoteException e) {
            wrapper = recoveryStrategy.recoverFromRemoteException(
                  wrapper, e);
            // If a RuntimeException is not thrown by the strategy, 
            // we will loop and try again
         }
      }
   }
}

invoke()'s main responsibility is, after some preparation, to tell the remote object's wrapper, the RemoteObjectWrapperImpl, to invoke the requested method on the wrapped object. It does this by calling the wrapper's invokeRemote() method. Note that the call is an RMI call—the only RMI call performed when using TRMI (apart from the calls hidden in trmi.Naming, of course). The call's parameters include the method name and parameter types, which the wrapper uses to locate the desired method. Why not simply pass the Method object, you ask? Because, as it turns out, Method doesn't implement Serializable, and so cannot be passed as a parameter to a remote method. Hence, we disassemble the requested method to its name and parameter types, only for the wrapper to reconstruct it on the server side.

invoke() normally ends in one of two ways:

  • The target method returns a value, in which case that value (response) returns to the user
  • The method raises an exception, in which case the exception propagates to the user through the InvocationTargetException

Now let's step back and look at how invoke() prepares for the invokeRemote() call. We first tackle the primitives problem:

Related:
1 2 Page 1
Page 1 of 2