DekGenius.com
[ Team LiB ] Previous Section Next Section

9.4 Remote Method Invocation

The object is the center of the Java world. Distributed object technologies provide the infrastructure that enables two objects running on two different machines to talk to each other using an object-oriented paradigm. Using traditional networking, you need to write IP socket code to let two objects on different machines communicate. While the socket-based approach works, it is prone to error. The ideal solution is to let the Java virtual machine do the work. You call a method in an object, and the virtual machine determines where the object is located. If it is a remote object, it will perform all the dirty network work for you.

Several technologies, like the Common Object Request Broker Architecture (CORBA), predate Java. CORBA enables developers to provide a clean, distributed programming architecture. CORBA has a very wide reach and is wrought with complexities associated with its grandiose goals. For example, it supports applications whose distributed components are written in different languages. In order to support everything from writing an object interface in C to handling more traditional object languages such as Java and Smalltalk, it has built up an architecture with a very steep learning curve.

CORBA does its job very well, but it does a lot more than you need in a pure Java environment. This extra functionality has a cost in terms of programming complexity. Unlike other programming languages, Java has distributed support built into its core. Borrowing heavily from CORBA, Java supports a simpler, pure Java distributed object solution called RMI.

9.4.1 The Structure of RMI

RMI is an API that enables you to ignore the fact that you have objects distributed all across the network. You write Java code that calls methods in remote objects using the same semantics you use in calling local methods. The biggest problem with providing this kind of API is that you are dealing with two separate virtual machines existing in two separate memory address spaces. Consider, for example, the situation in which you have a Bat object that calls hit( ) in a Ball instance. Located together on the same virtual machine, the method call looks like this:

ball.hit( );

You want RMI to use the exact same syntax when the Bat instance is on one machine and the Ball instance is on another. The problem is that the Ball instance does not exist inside the client's memory. How can you possibly trigger an event in an object to which there is no reference? The first step is to get a reference.

9.4.1.1 Remote object access

I am going to co-opt the term server for a minute and use it to refer to the virtual machine that holds the real copies of one or more distributed objects. In a distributed object system, you can have a single host (generally called an application server) act as an object server—a place from which clients get remote objects—or you can have all systems act as object servers. Clients simply need to be aware of where the object server(s) is located.[4] An object server has a single defining function: to make objects available to remote clients.

[4] Using JNDI, they do not even need to know where the server is. Clients just look up objects by name, and the naming and directory service knows where the server is. You will see this in practice later in the chapter when you read about EJB.

A special program called rmiregistry that comes with the JDK listens to a port on the object server's machine. The object server in turn binds object instances to that port using a special URL so clients can later find it. The format of the RMI URL is rmi://server/object. A client then uses that URL to find a desired object. For the previous bat and ball example, the ball would be bound to rmi://athens.imaginary.com/Ball. An object server binds an object to a URL by calling the static rebind( ) method of java.rmi.Naming:

Naming.rebind("rmi://athens.imaginary.com/Ball", new BallImpl( ));

The rmi://athens.imaginary.com portion of the preceding URL is self-evident; you cannot bind an object instance to a URL on another machine in a secure environment. Naming allows you to rebind an object using only the object name for short:

Naming.rebind("Ball", new BallImpl( ));

In RMI, binding is the process of associating an object with an RMI URL. The rebind( ) method specifically creates this association. At this point, the object is registered with the rmiregistry application and is available to client systems. Reference by any system to its URL is thus specifically a reference to the bound object.


The rebind( ) methods make a specific object instance available to remote objects that do a lookup on the object's URL. This is where life gets complicated. When a client connects to the object URL, it cannot get the object bound to that URL. That object exists only in the memory of the server. The client needs a way to fool itself into thinking it has the object while routing all method calls in that object over to the real object. RMI uses Java interfaces to provide this sort of hocus-pocus.

9.4.1.2 Remote interfaces

All Java objects that you intend to make available as distributed objects must implement an interface that extends the RMI interface java.rmi.Remote. You call this step making an object remote. You might do a quick double take if you look at the source code for java.rmi.Remote. It looks like this:

package java.rmi;
   
public interface Remote {
}

No, there is no typo there. The interface prescribes no methods to be implemented. It exists so that objects in the virtual machines on both the local and remote systems have a common base class they can use for deriving all remote objects. They need this base class since the RMI methods look for subclasses of Remote as arguments.

When you write a remote object, you have to create an interface that extends Remote and specify all methods that can be called remotely. Each of these methods must throw a RemoteException in addition to any application-specific exceptions. In the bat and ball example, you might have had the following interface:

public interface Ball extends java.rmi.Remote {
    void hit( ) throws java.rmi.RemoteException;
   
    int getPosition( ) throws RemoteException;
}

The BallImpl class implements Ball. It might look like:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
   
public class BallImpl
extends UnicastRemoteObject implements Ball {
    private int position = 0;
   
    public Ball( ) throws RemoteException {
        super( );
    }
   
    public int getPosition( ) {
        return position;
    }
   
    public void hit( ) {
        position += calculateDistance( );
    }
   
    protected int calculateDistance( ) {
        return 10;
    }
}

The java.rmi.server.UnicastRemoteObject class that the BallImpl extends provides support for exporting the ball; that is, it allows the virtual machine to make it available to remote systems. This may look like what the Naming class does, but it has a different purpose. Naming ensures that the object is bound to a particular URL, while exporting an object enables it to be referenced across the network. This means that you can pass the object as a method argument or return it as a return value. It also means that you can use Naming.rebind( ) to make the object available through a URL lookup. A URL lookup looks like this:

ball = (Ball)Naming.lookup("rmi://athens.imaginary.com/Ball");

Because you have just read about JNDI, you might wonder why RMI forces you to know where the object is located instead of using a simple JNDI name. The answer is simple: RMI predates JNDI. JNDI now, however, offers a service provider supporting RMI lookups.


Because you may not have the option of extending UnicastRemoteObject, you can export your objects another way using this syntax in the object constructor:

public BallImpl( ) throws RemoteException {
    super( );
    UnicastRemoteObject.exportObject(this);
}

Both approaches are equally valid. The only difference is the structure of your inheritance tree.

After writing both classes, you compile them just like any other object. This will, of course, generate two .class files, Ball.class and BallImpl.class.The final step in making the BallImpl class distributed is to run the RMI compiler, rmic, against it. In this case, run rmic using the following command line:

rmic BallImpl

Like the java command—and unlike the javac command—rmic takes a fully qualified class name as an argument. This means that if you had the Ball class in a package called baseball, you would run rmic as:

rmic -d classdir baseball.Ball

In this case, classdir represents whatever the root directory for your baseball package class files is. This directory will likely be in your CLASSPATH. The output of rmic will be two classes: Ball_Skel.class (the skeleton) and Ball_Stub.class (the stub). These classes will be placed relative to the classdir you specified on the command line.

9.4.1.3 Stubs and skeletons

I have introduced a couple of concepts, stub and skeleton, without any explanation. They are two objects you should never have to concern yourself with, but they do all of the heavy lifting that makes remote method calls work. In Figure 9-2, I show where these two objects fit in a remote method call.

Figure 9-2. The process of calling a method in a remote object
figs/jdbp_0902.gif

The process of translating a remote method call into network format is called marshaling; the reverse is called unmarshaling. When you run the rmic command on your remote-enabled classes, it generates two classes that perform the tasks of marshaling and unmarshaling. The first of these is the stub object, a special object that implements all of the remote interfaces implemented by the remote object. The difference is that where the remote object actually performs the business logic associated with a method, the stub takes the arguments to the method and sends them across the network to the skeleton object on the server. In other words, it marshals the method parameters and sends them to the server. The skeleton object, in turn, unmarshals those parameters; it takes the raw data from the network, translates it into Java objects, and then calls the proper method in the remote object.

The skeleton and stub perform the reverse roles for return values. The skeleton takes the return value from the method and sends it across the network. The client stub then takes the raw socket data and turns it into Java data, returning that Java data to the calling method.

9.4.1.4 Remote exceptions

All methods that can be called remotely and all constructors for remote objects must throw a special exception called java.rmi.RemoteException. The methods you write will never explicitly throw this exception. Instead, the local virtual machine will throw it when you encounter a network error during a remote method call. Examples of such situations include one of the machines crashing or a loss of connectivity between the two machines.

A RemoteException is unlike any other exception. When you write an application to be run on a single virtual machine, you know that if your code is solid, you can predict potential exceptional situations and where they might occur. You can count on no such predictability with a RemoteException. It can happen at any time during the course of a remote method call, and you may have no way of knowing why it happened. You therefore need to write your application code with the knowledge that at any point your code can fail for no discernible reason and have contingencies to support such failures.

9.4.2 Object Serialization

Not all objects that you pass between virtual machines are remote. In fact, you need to be able to pass the primitive Java data types as well as many basic Java objects, such as String or HashMap, that are not remote. When a nonremote object is passed across virtual machine boundaries, it gets passed by value using object serialization instead of the traditional Java RMI way of passing objects, by reference. Object serialization is a feature that enables you to turn objects into a data stream that you can use the way you use other Java streams—send it to a file, over a network, or to standard output. What is important about this method of passing objects across virtual machines is that changes you make to the object on one virtual machine are not reflected in the other virtual machine.

Most of the core Java classes are serializable. If you wish to build classes that are not remote but need to be passed across virtual machines, you need to make those classes serializable. A serializable class minimally needs to implement java.io.Serializable. For almost any kind of nonsensitive data you may want to serialize, just implementing Serializable is enough. You do not even need to write a method; Object already handles the serialization for you. It will, however, assume that you do not want the object to be serializable unless you implement Serializable. Example 9-7 provides a simple example of how object serialization works. When you run it, you will see the SerialDemo instance in the second block display the values of the one created in the first block.

Example 9-7. A simple demonstration of object serialization
import java.io.*;
   
public class SerialDemo implements Serializable {
    static public void main(String[  ] args) {
        try {
            { // Save a SerialDemo object with a value of 5
                FileOutputStream f = new FileOutputStream("/tmp/testing.ser");
                ObjectOutputStream s  = new ObjectOutputStream(f);
                SerialDemo demo = new SerialDemo(5);
   
                s.writeObject(demo);
                s.flush( );
            }
            { // Now restore it and look at the value
                FileInputStream f = new FileInputStream("/tmp/testing.ser");
                ObjectInputStream s = new ObjectInputStream(f);
                SerialDemo demo = (SerialDemo)s.readObject( );
   
                System.out.println("SerialDemo.getVal( ) is: " +
                                      demo.getVal( ));
            }
        }
        catch( Exception e ) {
            e.printStackTrace( );
        }
    }
   
    int test_val = 7; // value defaults to 7
   
    public SerialDemo( ) {
        super( );
    }
   
    public SerialDemo(int x) {
        super( );
        test_val = x;
    }
   
    public int getVal( ) {
        return test_val;
    }
}
    [ Team LiB ] Previous Section Next Section