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 areInvocationHandler
s that translate local method calls to remote calls.- A
RemoteProxy
has aRemoteExceptionHandler
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 aRemoteProxy
, a newRemoteProxy
that implements the interface for which theDistant
was generated is created. The methodDistant.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 RemoteException
s. 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.