Monday, March 17, 2008

Understanding Java RMI: A Simple Tutorial

http://www.basilv.com/psd/blog/2006/java-rmi-tutorial


I recently needed to write a simple java client-server application, and decided to implement the communication mechanism using Java RMI (Remote Method Invocation). I’ve used RMI in the past indirectly (i.e. when coding EJB session beans), but never directly, so I turned to Google to see what RMI tutorials were available. I quickly found several including the Sun RMI tutorial, but I was unsatisfied with all of them. Most of them seemed to be written in the Java 1.1 / 1.2 days, and the Java platform has changed a lot since then. I wasn’t sure if what I was reading reflected the best way of doing things in the latest version (JDK 5.0). Many of the tutorials also included extra details I didn’t need initially. After reading some of the tutorials, the relevant Java APIs, and doing some prototyping, I was able to get a simple RMI client-server application up and running. In the remainder of this article, I will present this application. I assume the use of JDK 5.0.

The simple application is a remote task executor. The client calls the server supplying a task (essentially a Runnable) to execute. The server then executes the task and returns the result to the client. A task is represented by the following interface:

public interface Task extends Serializable {
public Object execute(Object argument);
}

(In order to use a Task remotely, its argument and return value must be serializable. But I am assuming that Task is also used in a non-remote setting, so have kept the method definition more generic.)

The first step is to implement a remote interface that the client will use when communicating with the server:

public interface RemoteTaskExecutor extends Remote {
public Object executeTask(Task task, Serializable argument)
throws RemoteException;
}

The next step is to implement the class that implements this interface and will execute on the server.

public class TaskExecutorServer extends UnicastRemoteObject
implements RemoteTaskExecutor {

public TaskExecutorServer() throws RemoteException {
super();
}

public Object executeTask(Task task, Serializable argument) {
return task.execute(argument);
}
}

In versions of Java prior to 5.0, you then had to create a client stub class that implements the RemoteTaskExecutor interface and is called by the client (using the rmic compiler). However, in JDK 5.0 this is no longer necessary: a dynamic proxy (java.lang.reflect.Proxy) is automatically created instead. It is likely in a future version of Java that there will be a @Remote annotation to simplify constructing remoteable objects (i.e. removing the need for a separate remote interface). Such an annotation does not exist as of JDK 5.0, but the new EJB 3 specification is heading in that direction.

The next step is to configure the server to accept connections. The easiest way to do this is to start the RMI registry and register an instance of TaskExecutorServer with the registry using a specific name. Clients will be able to connect to this registry and obtain the remote proxy using this name. The registry can be started as a separate application (rmiregistry) or launched directly from the server application. Here’s the main method (in the TaskExecutorServer class) to do just that:

  public static final String REGISTRY_NAME =
TaskExecutorServer.class.getName();

public static void main(String[] args) throws Exception {
int registryPortNumber = 1099;

// Start RMI registry
LocateRegistry.createRegistry(registryPortNumber);

Naming.rebind(REGISTRY_NAME, new TaskExecutorServer());
System.out.println("Server running...");
}

The server application will not terminate, despite the main method completing execution. This is because the RMI code has started a daemon thread which is waiting to receive requests from clients. Each client request results in a new thread being spawned to handle the request. I found it amazing that so much functionality could be achieved in so few lines of code.

We’re not done yet - we need to provide the client. The client needs to connect to the RMI registry, obtain the remote proxy of the TaskExecutorServer, and then call it. We need to specify the host name and port number of the RMI registry in order to connect to it.

  public static void main(String[] args) throws Exception {
String host = "localhost";
int portNumber = 1099;
String lookupName = "//" + host + ":" + portNumber + "/" +
TaskExecutorServer.REGISTRY_NAME;
RemoteTaskExecutor executor = (RemoteTaskExecutor)
Naming.lookup(lookupName);

System.out.println("Requesting task execution for " + args[0]);
Object result = executor.executeTask(new TestTask(), args[0]);
System.out.println("Task executed for " + result);
}

The test task used in the above code just prints some information to the console:

public class TestTask implements Task {
public Object execute(Object argument) {
System.out.println("Executing task for " + argument);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// Do nothing.
}
System.out.println("Executed task for " + argument);
return argument;
}
}

That completes the simple example of a Java RMI client-server application. I left out a few details. One important detail is that all arguments supplied to a remote method must be serializable. You may have noticed that the Task interface extends Serializable for just this reason. In the above example, when the TestTask instance is passed (serialized) to the server, the java VM on the server must have access to the TestTask class in order to deserialize the instance. Therefore, the TestTask class must be on the classpath for the server application. There may be times where you want to avoid this limitation: perhaps you don’t want to redeploy and restart your server application whenever you implement a new Task to execute. Java RMI supports this by allowing the class definition itself to be sent (serialized) from the client to the server. However, since this is a security risk (it allows arbitrary code to be executed on the server), a security manager is required to explicitly allow this. The code to set this up is quite simple, and just needs to be called on the server before calling Naming.rebind().

if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}

If you want to shut your server down, you just need to call Naming.unbind() passing in the name you used to register your code. (i.e. REGISTRY_NAME in the example above.) This call must be made from the server.

That concludes my RMI tutorial. I hope you found it helpful.

0 comments: