Writing Distributed Applications with Java RMI

by Daniel Hiltgen

What does RMI mean to Developers:

Java RMI, or Remote Method Invocation, allows you to invoke methods on Java objects residing in different virtual machines using standard Java method invocation syntax. These virtual machines can potentially be running on separate computers on the network. When you make a remote method call you aren't limited to just passing and returning Java's primitive types, but you can also pass Java objects.

RMI only allows you to invoke methods on remote Java objects. At first glance it may appear that this limitation is a weakness of RMI but it actually allows RMI to be far more powerful than other distributed architectures that attempt to operate across different languages. By limiting RMI to just Java objects, the Java Object Model that you're already familiar with can be retained[3]. This includes polymorphism as well as Java's ability to dynamically download code across the network. CORBA, or Common Object Request Broker Architecture, is another popular distributed architectures but it uses a different approach [1]. CORBA allows communication between different programming languages, but in order to allow this a least-common-denominator approach must be used. All remote objects must be based on an Interface Definition Language (IDL) which drastically limits the type of arguments that may be passed in method calls. IDL only allows primitive types or other remote references specified in IDL to be passed across the network. Conventional local objects can not be passed across the network. By using the IDL you can't retain your native object model across the network. This IDL also adds additional complexity to learning how to use CORBA. RMI is quite easy to learn due to it's close alignment with the Java Object Model, but don't let this initial ease of use fool you. It is an extremely powerful system.

RMI was developed as a research project by Sun Microsystems' Sun Laboratories over many years. They looked at various distributed architectures and pooled these experiences together to create a better distributed system. For more information about the history and development of Java RMI see [3] and [4]. One of the key points that they discovered was that most previous distributed systems made the mistake of trying to make the difference between remote objects and local objects transparent to the programmer.

When you invoke a method on a remote object residing on a machine across the network different types of errors occur than when you invoke a method on a local object. RMI forces you to acknowledge this difference between local and remote failure modes by catching additional exceptions. This forces you to design your systems to recover from partial failure, thus allowing you to make more reliable distributed applications.

One of the strongest points of Java is it's platform independent nature. By using a virtual machine, binary code can be generated once and will run anywhere a VM is present. RMI leverages this platform independency to allow complete Java objects to be passed across the network including both their data and functionality. This means that you can send an object across the network and actually have it execute on a remote machine.

By combining RMIs ability to polymorphically pass objects in method invocations, with Java's ability to dynamically download code on demand a very interesting phenomenon happens. In a standard client-server system, if the server is upgraded the client must also be upgraded. With RMI, if you use polymorphism to extend your server side objects, then your clients will automatically download the latest code whenever they need it without requiring a newer version of the client software to be developed and deployed. With polymorphism and dynamic code downloading RMI allows you to use well known object-oriented design patterns, like those detailed in [2] in your distributed designs.

As you can see, RMI unlocks many doors for designing and building distributed systems and allows you to be free to design your distributed applications using all of the power of object orientation. Now that we've gone over what RMI means to developers, lets look into how it works from a developers perspective, and then we'll look at some example RMI programs.

RMI Programming Basics:

RMI is based on a client-server architecture. As a programmer you develop an RMI server that provides a set of well known methods that clients can invoke. Clients are then developed based on this well known set of methods. RMI uses a Java interface to provide the collection of well known methods. To be an RMI interface, your interface simply has to extend java.rmi.Remote. Clients can also be set up as RMI servers to allow remote callbacks to take place.

In the RMI world there are two types of objects, local and remote. Remote objects are objects that implement at least one RMI Interface. Local objects are all other objects that do not implement java.rmi.Remote. When you pass local objects in an RMI call they are passed by copy. An identical copy of the complete object graph for that object is made on the remote VM when the call is made. When a remote object is passed in an RMI call it is passed by reference. The entire remote object is not copied into the remote VM, but rather a proxy object is created that points back across the network to the actual object. If you modify the state of a local object passed in an RMI call the original object is not affected. If you modify the state of a remote object in an RMI call the original object is affected.

RMI uses serialization to pass objects across the network [6]. Serialization is a mechanism that allows complete object graphs to be written to a stream. The data on the stream can then be used to re-create the object with both its data and object graph intact. For example you can create a Vector, store a large collection of objects within that Vector, and then write the Vector out using serialization. When you read that Vector back in using serialization, you automatically get all of the contained objects back with their respective data intact. Serialization can also be used quite easily for persistent storage by writing an object to a file. By using the Vector approach, you can store all of your data in one method invocation, and then restore all of your data in one method invocation. While this may not be the most efficient approach as it doesn't allow random-access, it does work quite well for quick and dirty storage.

RMI provides a simple bootstrap server called rmiregistry that allows your clients to get their initial reference to your RMI server object. Once a client has a reference to the remote RMI server any methods from the well known interface may be invoked. The reference that is returned from the rmiregistry is actually a proxy to the real server object. RMI provides a simple compiler called rmic that generates this proxy code for you automatically. After you compile the server that implements your remote interface you simply run rmic on the server object and it generates a client side Stub and a server side Skeleton. The Stub and Skeleton take care of the actual communication between the two VMs.

When designing distributed applications an additional point that should be kept in mind is multi-threading. When you create an RMI server, in theory multiple clients can make simultaneous requests. You must make sure that all of your remote methods are designed in such a way as to be thread safe.

One of the benefits of RMI is that it is designed using multiple layers. As a developer you can choose how deep you wish to understand the system. This paper only discusses the application layer of RMI, which is all you really need to understand to write basic applications using RMI. There are other layers that handle the transport protocol and the way in which remote references are handled. For more information about how these different layers work and how you can go about modifying the way they function take a look at [5].

With this basic understanding of how RMI works we are ready to look at some example programs. We'll start out with a simple object passing client-server application followed by a polymorphic object passing example. We'll then look at a callback example where the server makes RMI calls back to the client program. For our final example we'll look at a sample that implements a well known object-oriented design pattern as documented by [2]. All of the following examples will work with JDK version 1.1 or later.

Simple Client-Server Object Passing Example:

The following example illustrates a simple Client-Server application passing a Java object through a Remote Method Invocation. This example shows a simple server that would have a queue of jobs to be performed by processor machines. The client application would run on the processing machines, and contact the server to download a Job object to be processed. This design could be used to farmout large collections of jobs to a collection of client machines, such as individual rendering jobs for an animation sequence.

Job.java:

This class represents a simple object that does some processing. In order to be passed in an RMI call objects must implement java.io.Serializable. Serializable is an empty interface that simply allows you to control which objects may be passed across the network and which objects may not.
public class Job implements java.io.Serializable{
	String msg;
	public Job(){
		//Just to distinguish one job from the next
		msg = Long.toString(System.currentTimeMillis());
	}
	public void process(){
		//Put complex algorithm here
		System.out.println("Job generated on server at: "+msg);
	}
}

JobQueue.java:

This interface represents the methods that may be remotely invoked on the server. Every RMI server must implement at least one interface that extends java.rmi.Remote.
public interface JobQueue extends java.rmi.Remote{
	//Get the next job on the queue
	Job getJob() throws java.rmi.RemoteException;
}

JobQueueImpl.java:

This class is one possible implementation of the remote interface above. This class represents the actual RMI server. You will notice that this object extends UnicastRemoteObject. UnicastRemoteObject extends RemoteObject and handles setting up the object as a remote server. This includes setting up the Stubs and Skeletons as well as overriding a few methods from java.lang.Object that must be modified to function properly in a distributed system. These methods include equals, hashCode, and toString. By overriding these methods remote references will act properly for clients of this server.
import java.rmi.*;
import java.rmi.server.*;

public class JobQueueImpl extends UnicastRemoteObject 
			implements JobQueue {

	public JobQueueImpl() throws RemoteException {
		super();
		try {
			Naming.bind("rmi://localhost/JobQueue",this);
			System.out.println("Server Ready");
		} catch (Exception e){
			System.err.println("Failed to start RMI server: "
				+ e.getMessage());
		}
	}
Naming.bind serves to bind this object to the rmiregistry running on the host named ``localhost''. By default the registry runs on port 1099 and bind uses this default port number. The argument to bind is in URL format so an alternate port number can be specified using standard URL syntax, and if any arguments are omitted default values will be used. For the future examples the hostname will be left blank so that the example programs will work without modifications on any hostname. The rmiregistry associates a textual name with each RMI server and this server has chosen to be called ``JobQueue.'' Clients will use this textual name to receive a remote reference to the server from the rmiregistry. If you attempt to bind twice to the same registry with the same name an exception will be thrown. The Naming class also provides a rebind method that will allow you to override a previous binding.
//JobQueueImpl.java continued...

	//The method that will be invoked remotely by clients
	public Job getJob(){
		return new Job();
	}

	public static void main(String[] args){
		try {
			new JobQueueImpl();
		} catch (RemoteException e){
			System.err.println("Failed to start RMI server: "
				+ e.getMessage());
		}
	}
}

Worker.java:

This class is a simple client that connects to the remote server and requests a Job. It then invokes the process method on the Job to perform the work.
import java.rmi.*;

public class Worker {
	public Worker(){
		try {
			//Get a reference to the remote server on this machine
			JobQueue queue = (JobQueue)Naming.lookup(
				"rmi:///JobQueue");

			//Request a job from the remote server
			Job myJob = queue.getJob();

			//Start processing the Job.
			myJob.process();
		} catch(Exception e){
			System.err.println("Error connecting to server: "
				+e.getMessage());
		}
	}

	public static void main(String[] args){
		new Worker();
	}
}
To run the above example code, copy the files into a directory and run the following commands:

Polymorphic RMI Example:

This example is an extension on the previous example. This example assumes that you may have more than one type of Job that you want processed by client machines. It illustrates how both local and remote objects can be polymorphically extended. By creating a new server object that extends from the previous JobQueueImpl we can re-use code from the old server. By extending the new Job types from the old Job type we don't need to modify any of the client code.

Matrix.java:

This class would be used to perform some sort of matrix manipulation. It is based on Job.java so it does not need to explicitly implement Serializable to be passed as an argument in a remote method invocation.
public class Matrix extends Job {
	public void process(){
		System.out.println("Would be processing a matrix");
	}
}

Prime.java:

This class is just another class that extends from Job.java.
public class Prime extends Job {
	public void process(){
		System.out.println("Would be computing primes");
	}
}

PolyJobQueueImpl.java:

This server is derived from the original JobQueueImpl class and re-uses the rmiregistry binding portion of the code. Main is re-written to make sure that an instance of this object is created instead of the old JobQueueImpl object. The getJob method is also modified to return the new types of Jobs.
import java.rmi.*;
import java.rmi.server.*;

public class PolyJobQueueImpl extends JobQueueImpl {
	int index = 1;
	Job[] list = { new Matrix() , new Prime() };

	public PolyJobQueueImpl() throws RemoteException {
		//JobQueueImpl takes care of the binding for us
		super();
	}
	
	//Polymorphically extend getJob to return the new Job types
	public Job getJob(){
		index = (index+1) % list.length; //Walk the index
		return list[index];
	}

	public static void main(String[] args){
		try {
			new PolyJobQueueImpl();
		} catch (RemoteException e){
			System.err.println("Failed to start RMI server: "
				+ e.getMessage());
		}
	}
}
To run the above example, simply place the three new files into the same directory as the first example, then run: You'll notice that the old Client, Job, and JobQueue files were not recompiled nor was the RMI compiler re-run. Due to the power of object orientation and RMIs close alignment with the Java object model these files don't need to be re-compiled because they did not change. Polymorphism and runtime binding take care of the changes for you. If the client software had already been installed on remote client machines this change to the server software would not require an upgrade of the clients in any way.

Example Showing Callbacks in RMI:

In the previous examples the clients were actively making requests for jobs from the passive server. In this example the roles are reversed. The client makes an initial connection to the server to register as a worker and then become passive. The server actively pushes jobs to any registered workers. This setup would allow a collection of clients to all register with a server and wait for jobs to become available. As the server collects jobs it would push them out to whichever clients are idle.

For simplicity sake this example only allows a single client to register but could be easily modified to store a collection of clients. As this is a fairly drastic change in the design of this example, most of the code has changed. The only code that remains the same is the Job class.

Another subtle point that this example shows is how RMI servers don't necessarily have to extend UnicastRemoteObject. They can simply implement an RMI interface and make a call to a static method of UnicastRemoteObject to export themselves. This is useful when your program must extend some other object, like Applet or GenericServlet. When UnicastRemoteObject is not extended equals, hashCode, and toString should be overridden to make sure that clients will function properly. For more information about how to properly extend these methods refer to the [5]. In this example these methods have not been overridden for simplicity sake.

CallbackJobQueue.java:

public interface CallbackJobQueue extends java.rmi.Remote{
	void register(CallbackWorker aClient) throws java.rmi.RemoteException;
}

CallbackJobQueueImpl.java:

import java.rmi.*;
import java.rmi.server.*;

public class CallbackJobQueueImpl extends UnicastRemoteObject 
			implements CallbackJobQueue {

	CallbackWorker client;

	public CallbackJobQueueImpl() throws RemoteException {
		super();
		try {
			//Bind to the registry running on this machine
			Naming.bind("rmi:///JobQueue",this);
			System.out.println("Server Ready");
		} catch (Exception e){
			System.err.println("Failed to start RMI server: "
				+ e.getMessage());
		}
		//Loop forever and give out jobs
		while(true){
			if (client != null){
				client.processJob(new Job());
			}
			try {
				Thread.sleep(1000); //Sleep for a sec.
			} catch(InterruptedException e){}
		}
	}

	public void register(CallbackWorker aClient){
		client = aClient;
	}

	public static void main(String[] args){
		try {
			new CallbackJobQueueImpl();
		} catch (RemoteException e){
			System.err.println("Failure with RMI server: "
				+ e.getMessage());
		}
	}
}

CallbackWorker.java:

This is the remote Interface that the CallbackWorkerImpl must implement to be an RMI server. This allows the CallbackJobQueueImpl server to be able to remotely invoke methods on the CallbackWorkerImpl client.
public interface CallbackWorker extends java.rmi.Remote{
	public void processJob(Job newJob) throws java.rmi.RemoteException;
}

CallbackWorkerImpl.java:

This class is the implementation of the CallbackWorker client. This client object also functions as an RMI server to allow callbacks to take place.
import java.rmi.*;
import java.rmi.server.*;

public class CallbackWorkerImpl implements CallbackWorker{
	public CallbackWorkerImpl(){
		try {
			UnicastRemoteObject.exportObject(this);
			CallbackJobQueue server = 
				(CallbackJobQueue)Naming.lookup(
					"rmi:///JobQueue");
			//Register with the server, then just wait for jobs
			server.register(this); 
		} catch (Exception e){
			System.err.println("Failed to setup for RMI");
		}
	}

	public void processJob(Job newJob){
		newJob.process();
	}

	public static void main(String[] args){
		new CallbackWorkerImpl();
	}
}
To run the above example, place all of the code into the directory with the previous examples and run the following commands: The careful reader should have noticed a few subtle errors in the design of this example. The CallbackJobQueueImpl constructor never finishes. This object should probably start a secondary thread to sit in the infinite loop passing out jobs. Another subtle problem is the processJob method of the client will block until the Job finishes processing. This would be undesirable if the server is trying to have multiple clients working at the same time. To resolve this deficiency the server could start one thread per client, or the Job object could start a secondary thread to perform the processing on the client side.

Object-Oriented Design Pattern Example:

The final example illustrates how well known object-oriented design patterns can be implemented in RMI. This example is based on the Abstract Factory example illustrated in [2]. In their example they show how an Abstract Factory can be used to create widgets for a GUI environment. By designing the client to use the Abstract Factory interface, different factories can be created to generate different sets of widgets without requiring changes to the clients. This example is not intended to illustrate a useful implementation of Abstract Factory, as Java's AWT and JFC do the work for you, but it does show how easily design patterns can be implemented in RMI. One other point to notice is how the pattern is split between client and server. This split was arbitrarily chosen, and with RMIs object-oriented nature you could decide to make the split in the pattern elsewhere.

WidgetFactory.java:

This class represents the RMI server interface for creating GUI widgets. The actual RMI server that implements this interface determines which type of Widget objects are returned to the clients.
public interface WidgetFactory extends java.rmi.Remote {
	public ScrollBar createScrollBar() throws java.rmi.RemoteException;
	public Window createWindow() throws java.rmi.RemoteException;
}

ScrollBar.java:

This interface represents an abstract ScrollBar that would be returned from a WidgetFactory server.
public interface ScrollBar extends java.io.Serializable{
	public String toString();
}

Window.java:

This interface represents another GUI component that can be returned from a WidgetFactory.
public interface Window extends java.io.Serializable {
	public String toString();
}

Client.java:

This class is a simple client of the WidgetFactory that will grab a Window and ScrollBar and display the actual type of object that was loaded.
import java.rmi.*;

public class Client {
	public static void main(String[] args){
		try {
			WidgetFactory factory = (WidgetFactory)
				Naming.lookup("rmi:///WidgetFactory");
			System.out.println("Window: "+factory.createWindow());
			System.out.println("ScrollBar: "+factory.createScrollBar());
		} catch(Exception e){
			System.err.println("Failure creating widgets: "
				+ e.getMessage());
		}
	}
}

MotifWidgetFactory.java:

This class is an implementation of the WidgetFactory that will return MotifWidgets.
import java.rmi.*;
import java.rmi.server.*;

public class MotifWidgetFactory extends UnicastRemoteObject
		implements WidgetFactory {
	
	public MotifWidgetFactory() throws RemoteException {
		super();
		try {
			Naming.rebind("rmi:///WidgetFactory",this);
			System.out.println("Server ready");
		} catch (Exception e){
			System.err.println("Failure setting up WidgetFactory: "
				+ e.getMessage());
		}
	}

	public static void main(String[] args){
		try {	
			new MotifWidgetFactory();
		} catch(Exception e){
			System.err.println("Failure creating WidgetFactory: "
				+ e.getMessage());
		}
	}

	//Remote methods
	public Window createWindow(){
		return new MotifWindow();
	}

	public ScrollBar createScrollBar(){
		return new MotifScrollBar();
	}
}

MotifScrollBar.java:

This class is a Motif representation of a ScrollBar that will be returned by the MotifWidgetFactory.
public class MotifScrollBar implements ScrollBar {
	public String toString(){
		return "MotifScrollBar";
	}
}

MotifWindow.java:

This class is a Motif representation of a Window that will be returned by the MotifWidgetFactory.
public class MotifWindow implements Window {
	public String toString(){
		return "MotifWindow";
	}
}

AWTWidgetFactory.java:

This is a second implementation of a WidgetFactory that returns AWT widgets.
import java.rmi.*;
import java.rmi.server.*;

public class AWTWidgetFactory extends UnicastRemoteObject
			implements WidgetFactory {
	public AWTWidgetFactory() throws RemoteException{
		super();
		try {
			Naming.rebind("rmi:///WidgetFactory",this);
			System.out.println("Server ready");
		} catch(Exception e){
			System.err.println("Failure setting up WidgetFactory: "
				+ e.getMessage());
		}
	}

	public static void main(String[] args){
		try {
			new AWTWidgetFactory();
		} catch(Exception e){
			System.err.println("Failure creating WidgetFactory: "
				+ e.getMessage());
		}
	}

	//Remote methods
	public Window createWindow(){
		return new AWTWindow();
	}

	public ScrollBar createScrollBar(){
		return new AWTScrollBar();
	}
}

AWTScrollBar.java:

public class AWTScrollBar implements ScrollBar {
	public String toString(){
		return "AWTScrollBar";
	}
}

AWTWindow.java:

public class AWTWindow implements Window {
	public String toString(){
		return "AWTWindow";
	}
}
To run this example place these files into a directory and run the following commands:

References:

1
The Common Object Request Broker: Architecture and Specification The Object Management Group, July 1995.

2
Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software Addison-Wesley Reading, Mass. 1995.

3
Waldo, J., Wyant, G., Wollrath, A., and Kendall, S. A Note on Distributed Computing Sun Microsystems Laboratories technical report SMLI TR-94-29, November 1994.

4
Wollrath, A., Riggs, R., and Waldo, J. A Distributed Object Model for Java Proceedings of the Fall USENIX Conference 1996.

5
Java Remote Method Invocation Specification JavaSoft, Sun Microsystems 1997.

6
Object Serialzation Specification JavaSoft, Sun Microsystems.


Daniel Hiltgen ( dhiltgen@toocool.calpoly.edu) is a third year computer science undergraduate at California Polytechnic State University in San Luis Obispo CA. He interned for six months at JavaSoft in Cupertino CA during spring and summer of 1997 where he worked on distributed RMI based applications.
( xxx ) as of 03/18/99