Defining Remote Objects
Now that you have a basic idea of how Java RMI works, we can explore the details of creating and using distributed objects with RMI. As I mentioned earlier, defining a remote RMI object involves specifying a remote interface for the object, then providing a class that implements this interface. The remote interface and implementation class are then used by RMI to generate a client stub and server skeleton for your remote object. The communication between local objects and remote objects is handled using these client stubs and server skeletons. The relationships among stubs, skeletons, and the objects that use them are shown in Figure 3-2.
Figure 3-2. Relationships among remote object, stub, and skeleton classes
When a client gets a reference to a remote object (details on how this reference is obtained come later) and then calls methods on this object reference, there needs to be a way for the method request to get transmitted back to the actual object on the remote server and for the results of the method call to get transmitted back to the client. This is what the generated stub and skeleton classes are for. They act as the communication link between the client and your exported remote object, making it seem to the client that the object actually exists within its Java VM.
The RMI compiler (rmic) automatically generates these stub and skeleton classes for you. Based on the remote interface and implementation class you provide, rmic generates stub and skeleton classes that implement the remote interface and act as go-betweens for the client application and the actual server object. For the client stub class, the compiler generates an implementation of each remote method that simply packages up (marshals) the method arguments and transmits them to the server. For the server skeleton class, the RMI compiler generates another set of implementations of the remote methods, but these are designed to receive the method arguments from the remote method call, unpackage them, and make the corresponding method call on the object implementation. Whatever the method call generates (return data or an exception), the results are packaged and transmitted back to the remote client. The client stub method (which is still executing at this point) unpackages the results and delivers them to the client as the result of its remote method call.
So, the first step in creating your remote objects is to define the remote interfaces for the types of objects you need to use in a distributed object context. This isn't much different from defining the public interfaces in a nondistributed application, with the following exceptions:
- Every object you want to distribute using RMI has to directly or indirectly extend an interface that extends the
java.rmi.Remoteinterface. - Every method in the remote interface has to declare that it throws a
java.rmi.RemoteExceptionor one of the parent classes ofRemoteException.[1][1]Note that prior to Java 1.2, the RMI specification required that every method on a remote interface had to throw
RemoteExceptionspecifically. In Java 1.2, this has been loosened to allow any superclass ofRemoteException. The reason for this change is to make it easier to define generic interfaces that support both local and remote objects.
RMI imposes the first requirement to allow it to differentiate quickly between objects that are enabled for remote distribution and those that are not. As we've already seen, during a remote method invocation, the RMI runtime system needs to be able to determine whether each argument to the remote method is a Remote object or not. The Remote interface, which is simply a tag interface that marks remote objects, makes it easy to perform this check.
The second requirement is needed to deal with errors that can happen during a remote session. When a client makes a method call on a remote object, any number of errors can occur, preventing the remote method call from completing. These include client-side errors (e.g., an argument can't be marshaled), errors during the transport of data between client and server (e.g., the network connection is dropped), and errors on the server side (e.g., the method throws a local exception that needs to be sent back to the remote caller). The RemoteException class is used by RMI as a base exception class for any of the different types of problems that might occur during a remote method call. Any method you declare in a Remote interface is assumed to be remotely callable, so every method has to declare that it might throw a RemoteException, or one of its parent interfaces.
Example 3-3 shows a simple remote interface that declares two methods: doThis() and doThat(). These methods could do anything that we want; in our Account example, we had remote methods to deposit, withdraw, and transfer funds. Each method takes a single String argument and returns a String result. Since we want to use this interface in an RMI setting, we've declared that the interface extends the Remote interface. In addition, each method is declared as throwing a RemoteException.
Example 3-3. The ThisOrThatServer Interface
import java.rmi.Remote; import java.rmi.RemoteException;
public interface ThisOrThatServer extends Remote {
public String doThis(String todo) throws RemoteException;
public String doThat(String todo) throws RemoteException;
}
With the remote interface defined, the next thing we need to do is write a class that implements the interface. Example 3-4 shows the ThisOrThatServerImpl class, which implements the ThisOrThatServer interface.
Example 3-4. Implementation of the ThisOrThatServer
import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException;
public class ThisOrThatServerImpl extends UnicastRemoteObject implements ThisOrThatServer {
public ThisOrThatServerImpl() throws RemoteException {} // Remotely accessible methods public String doThis(String todo) throws RemoteException {
return doSomething("this", todo);
}
public String doThat(String todo) throws RemoteException {
return doSomething("that", todo);
}
// Non-remote methods private String doSomething(String what, String todo) {
String result = "Did " + what + " to " + todo + ".";
return result;
}
}
This class has implementations of the doThis() and doThat() methods declared in the ThisOrThatServer interface; it also has a nonremote method, doSomething(), that is used to implement the two remote methods. Notice that the doSomething() method doesn't have to be declared as throwing a RemoteException, since it isn't a remotely callable method. Only the methods declared in the remote interface can be invoked remotely. Any other methods you include in your implementation class are considered nonremote (i.e., they are only callable from within the local Java virtual machine where the object exists).
Key RMI Classes for Remote Object Implementations
You probably noticed that our ThisOrThatServerImpl class also extends the UnicastRemoteObject class. This is a class in the java.rmi.server package that extends java.rmi.server.RemoteServer, which itself extends java.rmi.ser-ver.RemoteObject, the base class for all RMI remote objects. There are four key classes related to writing server object implementations:
RemoteObjectRemoteObjectimplements both theRemoteandjava.rmi.serverpackage, it is used by both theSerializableinterfaces. Although theRemoteObjectclass is in the client and server portions of a remote object reference. Both client stubs and server implementations are subclassed (directly or indirectly) fromRemoteObject. ARemoteObjectcontains the remote reference for a particular remote object.RemoteObjectis an abstract class that reimplements theequals(),hashCode(), andtoString()methods inherited fromObjectin a way that makes sense and is practical for remote objects. Theequals()method, for example, is implemented to returntrueif the internal remote references of the twoRemoteObjectobjects are equal, (i.e., if they both point to the same server object).RemoteServerRemoteServeris an abstract class that extendsRemoteObject. It defines a set of static methods that are useful for implementing server objects in RMI, and it acts as a base class for classes that define various semantics for remote objects. In principle, a remote object can behave according to a simple point-to-point reference scheme; it can have replicated copies of itself scattered across the network that need to be kept synchronized; or any number of other scenarios. JDK 1.1 supported only point-to-point, nonpersistent remote references with theUnicastRemoteObjectclass. The Java 2 SDK 1.2 has introduced the RMI activation system, so it provides another subclass ofRemoteServer,Activatable.UnicastRemoteObject- This is a concrete subclass of
RemoteServerthat implements point-to-point remote references over TCP/IP networks. These references are nonpersistent: remote references to a server object are only valid during the lifetime of the server object. Before the server object is created (inside a virtual machine running on the host) or after the object has been destroyed, a client can't obtain remote references to the object. In addition, if the virtual machine containing the object exits (intentionally or otherwise), any existing remote references on clients become invalid and generateRemoteExceptionobjects if used. Activatable- This concrete subclass of
RemoteServeris part of the new RMI object activation facility in Java 1.2 and can be found in thejava.rmi.activationpackage. It implements a server object that supports persistent remote references. If a remote method request is received on the server host for anActivatableobject, and the target object is not executing at the time, the object can be started automatically by the RMI activation daemon.