Java Tip 108: Apply RMI autogeneration

Write remote objects using only local semantics and "RMI-retrofit" existing local interfaces

When you design an application to use RMI as a deployment option, you must follow remote semantics at compile time. Interfaces that may need to be remotely accessible must extend java.rmi.Remote, and their methods must throw a java.rmi.RemoteException. This can be quite intrusive. On the server side, try/catch blocks that deal with RemoteException proliferate, even though the methods execute in the virtual machine from which the call was made. On the client side, these try/catch blocks tend to get into the code for user interface elements, rather than being handled centrally. If the application is deployed as a standalone application, the try/catch blocks then are superfluous.

The throws RemoteException clause in the declaration of a class's remote method is misplaced. The possible exceptions occur in a transport mechanism that the class itself does not use. Some clients might access the class remotely; others might be in the same virtual machine. The remote class itself does not need to know how other classes will use it. These semantics contrast with those for I/O methods that really do rely on disc or network access and for which there is a throws IOException clause in the method declaration.

This article describes a tool, RMIAutogenerate, that allows objects written with local semantics to be made remote. The tool works by creating interfaces that have roughly the same method signatures as the local interfaces but are remote. These generated interfaces form a layer between the application server interfaces and the RMI runtime. Client-side translation from the generated interfaces to the application server interfaces is performed via runtime-generated proxy objects.

You may also use the tool to "RMI-retrofit" existing interfaces, even those for which no source code is available.

RMI 101

Consider the following simple RMI application. On the server side is an interface InterfaceA, an implementation AImpl, and a main class called Server. InterfaceA extends java.rmi.Remote and its implementation extends java.rmi.server.UnicastRemoteObject.

package rdr101;
import java.rmi.*;
public interface InterfaceA extends Remote {
  public String doA() throws RemoteException;
}
package rdr101;
import java.rmi.*;
import java.rmi.server.*;
public class AImpl extends UnicastRemoteObject implements InterfaceA {
  public String doA() throws RemoteException {
    return "doA done";
  }
package rdr101;
import java.rmi.*;
public class Server {
  public static void main( String[] args ) throws Exception {
    String hostName = java.net.InetAddress.getLocalHost().getHostName();
    int port = 8989;
    String regName = "//" + hostName +
    ":" + port + "/" + "TestServer";
    AImpl a = new AImpl();
    Naming.rebind( regName, a );
  }

On the client side, there is a single class called Client. In its main() method, Client gets a remote reference to an InterfaceA. A Client object is constructed with this reference.

package rdr101;
import java.rmi.*;
public class Client {
  private InterfaceA a;
  public Client( InterfaceA ia ) {
    a = ia;
  }
  public void doStuff() throws RemoteException {
    String str = a.doA();
    System.out.println( "doA: " + str );
  }
  public static void main( String[] args ) throws Exception {
    String hostName = java.net.InetAddress.getLocalHost().getHostName();
    int port = 8989;
    String regName = "//" + hostName +
      ":" + port + "/" + "TestServer";
    InterfaceA a = (InterfaceA) Naming.lookup( regName );
    Client client = new Client( a );
    client.doStuff();
  }

Using local semantics

The idea behind RMIAutogenerate is simply to put a layer between the application interfaces (and their implementations) and the RMI runtime. You rewrite InterfaceA and AImpl with local semantics. Then you create a remote interface RemoteInterfaceA and an implementation RemoteInterfaceAImpl. The single method of RemoteInterfaceA corresponds to the method of InterfaceA:

package rdr101.local;
public interface InterfaceA {
  public String doA();
}
package rdr101.local;
public class AImpl implements InterfaceA {
  public String doA() {
   return "doA done";
  }
package rdr101;
import java.rmi.*;
public interface RemoteInterfaceA extends Remote {
  public String remote_doA() throws RemoteException;
}

The implementation of RemoteInterfaceA simply delegates to an instance of InterfaceA:

package rdr101.local;
import java.rmi.*;
import java.rmi.server.*;
public class RemoteInterfaceAImpl extends UnicastRemoteObject
                         implements RemoteInterfaceA {
  private InterfaceA worker;
  public RemoteInterfaceAImpl( InterfaceA ia ) throws RemoteException {
    worker = ia;
  }
  public String remote_doA() throws RemoteException {
    return worker.doA();
  }

The server program is slightly different in that a RemoteInterfaceA, rather than an InterfaceA, is bound to the RMI runtime.

package rdr101.local;
import java.rmi.*;
public class Server {
  public static void main( String[] args ) throws Exception {
    String hostName = java.net.InetAddress.getLocalHost().getHostName();
    int port = 8989;
    String regName = "//" + hostName +
          ":" + port + "/" + "TestServer";
    AImpl a = new AImpl();
    RemoteInterfaceA ra = new RemoteInterfaceAImpl( a );
    Naming.rebind( regName, ra );
  }

On the client side, you use a proxy object that implements InterfaceA, but delegates its method calls to a RemoteInterfaceA:

package rdr101.local;
import java.rmi.*;
public class ProxyA implements InterfaceA {
  private RemoteInterfaceA remoteA;
  public ProxyA( RemoteInterfaceA ria ) {
    remoteA = ria;
  }
  public String doA() {
    try {
      return remoteA.remote_doA();
    } catch (RemoteException re) {
      re.printStackTrace();
      System.exit( 0 );
    }
  }

The client program now becomes:

package rdr101.local;
import java.rmi.*;
public class Client {
  private InterfaceA a;
  public Client( InterfaceA ia ) {
    a = ia;
  }
  public void doStuff() {
    String str = a.doA();
    System.out.println( "doA: " + str );
  }
  public static void main( String[] args ) throws Exception {
    String hostName = java.net.InetAddress.getLocalHost().getHostName();
    int port = 8989;
    String regName = "//" + hostName +
        ":" + port + "/" + "TestServer";
    RemoteInterfaceA remoteA = (RemoteInterfaceA) Naming.lookup( regName );
    InterfaceA proxy = new ProxyA( remoteA );
    Client client = new Client( proxy );
    client.doStuff();
  }

By writing some boilerplate code that forms a separate RMI layer, you are able to write your main application classes (InterfaceA, AImpl, and Client) with local semantics.

A complication

Of course, in a typical remote application, a number of remote interfaces exist. A reference to a main server interface is obtained by the lookup mechanism, and from that remote reference, other remote references to other interfaces are obtained. For example, suppose that InterfaceA has another method that returns a reference to an InterfaceB:

public InterfaceB getB();

Suppose that you wrote RemoteInterfaceB and its implementation RemoteInterfaceBImpl following the pattern described above, and that you wrote a proxy called ProxyB that implements InterfaceB and is created from a RemoteInterfaceB. You want ProxyA to return a ProxyB in its implementation of getB(). You can achieve this by having RemoteInterfaceA return a RemoteInterfaceB, rather than an InterfaceB, from its remote_getB() method. You can then write the getB() method in ProxyA:

public InterfaceB getB() {
  RemoteInterfaceB rib = null;
  try {
    rib = new ProxyB( remoteA.remote_getB() );
  } catch (RemoteException re) {
    //Handle the exception...
  }
  return rib;
}

This complication only means that when writing the boilerplate code you must keep in mind the entire set of local interfaces for which remote interfaces are being written.

Automatic class generation

The code that forms the RMI layer can be autogenerated from the following information:

  • The list of interfaces for which remote versions are to be created
  • For each such interface, the name of the implementing class
  • Details such as the package name and sourcepath for the generated files

On the client side, the proxy classes can also be automatically created. Rather than creating these at compile time, you can create them at runtime using the Proxy mechanism introduced in JDK 1.3.

Proxy classes

Proxy classes are a mechanism added to JDK 1.3, which lets you create a class at runtime that implements one or more given interfaces. A java.lang.reflect.Proxy is created using the method:

Proxy.newProxyInstance( ClassLoader loader,
    Class[] interfaces, InvocationHandler ih );

A java.lang.reflect.InvocationHandler must implement the method:

public Object invoke( Object proxy, Method m,
             Object[] args ) throws Throwable;

Method calls to a Proxy class are passed to the class's InvocationHandler invoke() method. You can find links to more information on the Proxy API in Resources.

RMIAutogenerate

The tool RMIAutogenerate is used to create an RMI layer automatically. It consists of four public classes and several helper classes. The public classes are:

  • RMIAutogenerate, which is used to create the client-side RMI layer. From a list of interfaces and their implementation classes, it creates .java files for corresponding remote classes. It then compiles these files and "RMI-compiles" the resulting class files.
  • RemoteProxy is used to create client-side proxy objects at runtime. The proxies created are InvocationHandlers that translate local method calls to remote calls.
  • A RemoteProxy has a RemoteExceptionHandler that is a central location on the client side for dealing with transport-layer errors.
  • The interface Distant is used to mark the autogenerated classes. When these are returned from remote method calls, within a RemoteProxy, a new RemoteProxy that implements the interface for which the Distant was generated is created. The method Distant.primaryInterfaceName() is used to identify the interface that the proxy must implement.

You can obtain the source code for this tool in Resources.

Conclusion

By introducing a translation layer between the main application classes and the RMI runtime, you can write applications without the tedium of lots of try/catch blocks for dealing with RemoteExceptions. You can use the RMIAutogenerate tool to create such a translation layer as compile-time generated classes on the server side and as runtime-generated proxy classes on the client side.

Dr. Tim Lavers is a senior software engineer at Pacific Knowledge Systems in Sydney, Australia. He is currently working on Knowledge Acquisition software that allows pathologists to produce expert systems that add patient-specific comments to test results.

Learn more about this topic