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:

Page 2 of 2
 private void convertPrimitiveParamTypes(
      Class[] types, 
      boolean[] primitiveTypes) 
      throws IllegalArgumentException {
   for (int i = 0; i < types.length; i++) {
      if (types[i].isPrimitive()) {
         primitiveTypes[i] = true;
         if (types[i].equals(Boolean.TYPE)) {
            types[i] = Boolean.class;
         } else if (types[i].equals(Character.TYPE)) {
            types[i] = Character.class;
         } else if (types[i].equals(Byte.TYPE)) {
            types[i] = Byte.class;
         } else if (types[i].equals(Short.TYPE)) {
            types[i] = Short.class;
         } else if (types[i].equals(Integer.TYPE)) {
            types[i] = Integer.class;
         } else if (types[i].equals(Long.TYPE)) {
            types[i] = Long.class;
         } else if (types[i].equals(Float.TYPE)) {
            types[i] = Float.class;
         } else if (types[i].equals(Double.TYPE)) {
            types[i] = Double.class;
         } else {
            throw new IllegalArgumentException(
                  "Unrecognized primitive parameter type: " 
                  + types[i]);
         }
      } else {
         primitiveTypes[i] = false;
      }
   }
}

The problem that produced the revolting workaround above is that, like Method, the Class objects for the primitive types (e.g., Integer.TYPE, the class that represents the int type) aren't Serializable. Thus we can't simply send the objects to the remote wrapper. Instead, convertPrimitiveParamTypes() must replace each primitive type with its object-oriented counterpart—int becomes Integer, short becomes Short, and so on—and send those. The method also marks the changed parameters in an array of booleans. Once the converted types are received, the remote wrapper uses the array to convert them back to their original, primitive representations.

Once this nasty affair ends, invoke() handles the case of non-Serializable, non-Remote parameters. When using RMI, parameters to remote methods divide into two types: Serializables are passed by value, while Remote objects are passed by reference (see the RMI tutorial for more details). Objects of other types cannot be sent to a remote method. TRMI is more lenient in this regard. The following snippet shows how TRMI handles method parameters:

 private void convertNonSerializableParams(
   Class[] types, Object[] params) {
   // Happens when the method is parameter-less
   if (params == null) {
      return;
   }
   for (int i = 0; i < params.length; i++) {
      // Skip the class if it can be handled by RMI 
      // (this includes TRMI stubs)
      if (Serializable.class.isAssignableFrom(
             params[i].getClass()) ||
          java.rmi.Remote.class.isAssignableFrom(
             params[i].getClass())) {
         continue;
      }
      // Create a stub if we can
      if (types[i].isInterface()) {
         params[i] = Naming.getStub(
            params[i], new Class[] {types[i]});
      } 
   }
}

RMI handles the Serializable or Remote objects, including TRMI stubs; if you obtain a TRMI stub by calling trmi.Naming.look() and pass it as a parameter to a remote method, the stub will correctly transmit over the network, and a similar stub referring to the same wrapped object will be deserialized on the server side.

Now comes TRMI's special treatment: For interface parameters (interfaces appearing in the remote method's declaration), trmi.Naming.getStub() creates a TRMI stub that wraps a local RemoteObjectWrapperImpl. The stub is the parameter actually passed to the server side. This means that if you say, for example:

 Server myRemoteServer = trmi.Naming.lookup(name);
Hello callback = new HelloImpl();
myRemoteServer.performOperation(callback);

then a TRMI stub will be automagically created for callback, and the remote server will receive a reference to your now remotely exposed local object, even though you never declared callback to be a remote object of any size, shape, or form. This property is what makes TRMI truly transparent.

Finally, TRMI doesn't handle noninterface parameters; they will raise an exception when they reach RMI marshaling.

The gory details: Server side

Most server-side actions mirror the client's actions. As you'll recall, the server's main method, RemoteObjectWrapperImpl.invokeRemote(), actually invokes the methods on the wrapped object:

 public Object invokeRemote(
      String methodName, 
      Class[] paramTypes, 
      boolean[] primitiveparams, 
      Object[] params) 
   throws RemoteObjectWrapperException, RemoteException, 
   InvocationTargetException {
   // Get the method to be invoked.
   Method method = findMethod(
         exposedInterfaces, methodName, 
         paramTypes, primitiveparams);
   try {
      // Simply invoke the method on the wrapped object and 
      // return the result. If an exception is thrown, it is 
      // propagated.
      Object result = method.invoke(wrappedObject, params);
      return result;
   } catch (IllegalAccessException e) {
      // This is thrown by Method.invoke() itself.
      e.printStackTrace();
      // The method cannot be invoked.
      throw new RemoteObjectWrapperException(
            "Method '" + methodName + 
            "' could not be invoked: " + e, e);
   } catch (IllegalArgumentException e) {
      // This too is thrown by Method.invoke() itself
      e.printStackTrace();
      // The method cannot be invoked.
      throw new RemoteObjectWrapperException(
            "Method '" + methodName + 
            "' could not be invoked: " + e, e);
   }
}

First, invokeRemote() locates the method to be invoked, which involves converting the primitive types back to their original values, then looking up the method using reflection (look at the code to see how to do that). Once this step completes, Method.invoke() invokes the method. If the method returns a result, the result returns to the calling StubInvocationHandler. If, however, the method throws an exception (wrapped in an InvocationTargetException), the exception propagates to the caller.

Error recovery

TRMI allows centralized RMI error handling. The attentive reader has probably noticed a small, inconspicuous catch block in StubInvocationHandler.invoke():

 // ...
try {
   // ... 
   // Tell the wrapper to invoke the method
   Object response = wrapper.invokeRemote(
         method.getName(),
         convertedTypes,
         primitiveTypes,
         params);
} 
// ...
catch (RemoteException e) {
   wrapper = recoveryStrategy.recoverFromRemoteException(
         wrapper, e);
   // If a RuntimeException is not thrown by the strategy, 
   // we will loop and try again
}

So, if an error occurs while trying to invoke the method, StubInvocationHandler calls a recovery strategy—RemoteExceptionRecoveryStrategy—to lead itself out of the mess it got into. The strategy's interface contains a single method:

 public interface RemoteExceptionRecoveryStrategy 
extends java.io.Serializable {
      public RemoteObjectWrapper recoverFromRemoteException(
                  RemoteObjectWrapper currentWrapper,
                  RemoteException ex) 
         throws RuntimeException;
}

The strategy should recover from the error state by providing a new RemoteObjectWrapper to replace the one that failed. invoke() will then loop and try to reinvoke the method using the new wrapper. If, for some reason, the strategy cannot provide a new wrapper, it should throw a RemoteExceptionRecoveryStrategyException (a subclass of RuntimeException).

trmi.Naming provides the recovery strategy to StubInvocationHandler when the stub is created. A factory, RemoteExceptionRecoveryStrategyFactory, creates the strategy used for each object. The default strategy implementation is fairly simple:

 public class DefaultRemoteExceptionRecoveryStrategy
implements RemoteExceptionRecoveryStrategy {
   // ...
   public RemoteObjectWrapper recoverFromRemoteException(
         RemoteObjectWrapper currentWrapper,
         RemoteException ex) {
      sleep();
      return lookupRemoteObject(currentWrapper, ex);
   }
   protected RemoteObjectWrapper lookupRemoteObject(
         RemoteObjectWrapper currentWrapper,
         RemoteException ex) {
      if (name == null) {
         return currentWrapper;
      }
      try {
         RemoteObjectWrapper wrapper = 
            trmi.Naming.lookupRemoteObjectWrapper(name);
         return wrapper;
      } catch (Exception e) {
         return currentWrapper;
      }
   }
}

The strategy sleeps for a while, then tries to look up the remote object again in trmi.Naming (the strategy receives the object's URL in its constructor). If this fails, the current wrapper returns. The strategy can recover from a server crash: once the server is back online, the strategy will look up the object again and invoke the method. However, bear in mind that a server-crash recovery will work well only for stateless or persistent remote objects.

The user can replace the default recovery strategy with her own by writing the strategy and its factory, and then calling trmi.Naming.setRecoveryStrategyFactory(). Other strategy implementations can, for example, try an alternate host for looking up the object when the main host fails. The main point, however, is that the recovery strategy provides a centralized, encapsulated way of dealing with remote errors, leaving the rest of the system free to run the business logic.

Simplify distributed application creation

Transparent RMI solves RMI's problems. Any interface can be exposed remotely, not just those that extend Remote. Furthermore, interface methods are not limited—they don't have to throw RemoteExceptions. Error-handling code is transparent and centralized in the strategy, and remote calls look just like local method calls—even more so than in RMI, thanks to automatic stub creation. Interface implementations don't have to extend any specific classes, leaving them free to extend any class, as needed.

Due to these factors, you can use TRMI to create clean distributed applications, as well as to distribute existing single-VM components to multiple VMs with little effort.

Guy Gur-Ari is an undergraduate computer science student at Tel Aviv University in Israel. He is a self-taught programmer and spends much of his spare time working on Linux-based Java projects. His nonprofessional interests include playing squash and producing questionably harmonious sounds from his piano.

Learn more about this topic