13.3. Defining Remote ObjectsNow that you have a basic idea of how Java RMI works, we can explore the details of creating and using distributed objects with RMI in more detail. As 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 13-2. Figure 13-2. Relationships among remote object, stub, and skeleton classesWhen 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. Stubs and skeletons are generated from the classes that implement the remote RMI interfaces that you've exported. The process of generating these stub and skeleton classes is done either dynamically at runtime (in JDK 1.5 or later JVMs) or by using the RMI compiler . Whether you use the RMI compiler or use dynamic stub and skeleton generation, the role of these classes in the RMI runtime are the same. The stub and skeleton classes act as go-betweens for the client application and the actual server object, respectively. The client stub class implements the remote interface that you defined, and its implementations of each remote method package up (marshal) the method arguments provided by the client and transmit them to the server. The skeleton class is responsible for receiving the method arguments from the RMI runtime after they've been transmitted over the network. It unpackages (unmarshals) the method arguments and makes the corresponding method call on the actual remote object implementation on the server. Whatever response the method call generates (return data or an exception), the results are packaged and transmitted back to the remote client by the skeleton and the RMI runtime working together. 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. As we saw earlier in the chapter, 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:
RMI imposes the first requirement to allow it to differentiate quickly between objects that are enabled for remote distribution and those that aren't. 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. In Example 13-1 we saw the Account interface, a remote interface that declares six remote methods: getName( ), getBalance( ), withdraw( ), deposit( ) and TRansfer( ). 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 has arguments and return values that are either Remote or Serializable, and each method is declared as throwing a RemoteException. With the remote interface defined, the next thing we need to do is write a class that implements the interface. We saw the implementation of the Account interface, the AccountImpl class, in Example 13-2. This class has implementations of the six remote methods defined in the Account interface; it also has a nonremote method, checkTransfer( ), to verify that a funds transfer between two accounts can be made. Notice that the checkTransfer( ) method doesn't have to be declared as throwing a RemoteException, since it isn't a remotely callable method. Only the methods that participate in the remote method protocol of the RMI runtime need to declare that they can throw RemoteException or one of its parent exceptions. The methods in your implementation class that participate in the RMI runtime include implementations of any methods declared in the remote interface (withdraw( ), transfer( ), etc. in our example) and, if you're extending UnicastRemoteObject, any constructors on your implementation class. Any other methods you define in your implementation class (like checkTransfer( )) are considered nonremote (i.e., they are callable only from within the local Java virtual machine where the object exists). Constructors are required to have RemoteException in their throws clause because an RMI implementation class that extends UnicastRemoteObject is "exported" and made accessible to remote method invocations when it is constructed. The RemoteException in the throws clause of the constructor is required to cover any problems that may occur during the RMI export process. All of the constructors on UnicastRemoteObject throw RemoteException because they all call the static UnicastRemoteObject.exportObject( ) methods internally, so the Java compiler will force you to include RemoteException in the throws clause of your implementation class. 13.3.1. Key RMI Classes for Remote Object ImplementationsAs you have probably noticed, our AccountImpl 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.server.RemoteObject, the base class for all RMI remote objects. Four key classes are related to writing server object implementations:
|