Saturday, August 27, 2011

Using RMI without the Registry

Introduction


I was looking for a way to have one JVM talk to a child process that is also a JVM. Ideally, I'd simply like to call methods on the objects in the child JVM without too much hassles. I thought that RMI seemed like the perfect solution after reading through this great RMI tutorial from sun.

Then I found an even better RMI tutorial which simplifies things a bit and works great if you only care about the case where you have both 'client' and 'server' processes running on the same host.  Moreover this tutorial does things in Eclipse, which is a boon for me, since that's what I work with every day.

There was just one little thing that bothered me about the setup of the second tutorial. It still requires the use of the RMI registry for the client to obtain a reference to the server object. This is a tad annoying, because it means we still have to enter some shell command to start this service on the machine running both processes.


Is the RMI registry really necessary?

The short answer is no.

A longer explanation is that the registry is only required to get the 'first' remote object reference from the server to the client. From that point forward the client can 'talk' to this remote object by calling methods. That way you can send over objects as parameters, get other objects back as results etc. In all of this the registry plays absolutely no part. I found an article that explains this very well. Although unfortunately it seems to have been written in times when we still needed to use the RMI stub compiler. Nevertheless, conceptually, the explanation seems still correct.

So essentially, all you need is some way to bootstrap the process and 'send over' that first object without using the registry. That leaves us with the question:


How do we 'bootstrap' RMI without the registry

Reading the article, and Googling around a bit I found that several people were asking similar questions. Several of them suggested using Java serialization but mostly I just found questions from people having problems with that and no real solutions that I could easily implement. The article above was one of the best, but unfortunately the crucial bit of source code that actually does "the magic trick" seems to be missing!

So I thought it would be hard... but as it turns out actually it is quite simple. All you need to do is serialize your first stub-object (the very same one the server would normally register in the registry) and save that to a file. The client and server both have to agree on where that file is supposed to be. But that isn't a big problem in my scenario where both processes are on the same machine, and one of the processes is actually started by the other. For example, the location can be passed as a system property.

To show how it is done, I'll show you exactly which bits of code in the Eclipse RMI Tutorial need to be changed.

In the server class 'ComputeEngineStarter' there is this bit of code that creates the remote server object and registers it with a local RMI registry. Change it like this:

@Override
    public void doCustomRmiHandling() {
        try {
            Compute engine = new ComputeEngine();
            Compute engineStub = (Compute)UnicastRemoteObject.exportObject(engine, 0);
            
            //CHANGED: The normal way, using registry:
            //Registry registry = LocateRegistry.getRegistry();
            //registry.rebind(Compute.SERVICE_NAME, engineStub);
            
            //Using a file instead
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(Compute.SERIALIZED_STUB_FILE));
            try {
             out.writeObject(engineStub);
            } finally {
             out.close();
            }
        }
        catch(Exception e) {
            e.printStackTrace();
        }

    }


In the client class 'StartComputeTaskPI', we make a change:

@Override
    public void doCustomRmiHandling() {
        try {
            //CHANGED: extracted to a method:
            Compute compute = getComputeServer();

            PI task = new PI(50);
            Number pi = compute.executeTask(task);
            System.out.println("computed pi: " + pi);
        }
        catch(Exception e) {
            e.printStackTrace();
        }

    }

    private Compute getComputeServer() throws NotBoundException, FileNotFoundException, IOException, ClassNotFoundException {
        //CHANGED: The 'normal' way using the registry:
        //Registry registry = LocateRegistry.getRegistry();
        //Compute compute = (Compute)registry.lookup(Compute.SERVICE_NAME);
        //return compute;

        //Reading from a file instead: 
 ObjectInputStream in = new ObjectInputStream(new FileInputStream(Compute.SERIALIZED_STUB_FILE));
 try {
    return (Compute) in.readObject();
 } finally  {
     in.close();
 }
    }

Ofcourse you also need to define the constant that defines where the shared file lives. I'll leave that bit to you. Pick a location that works on your machine and os (e.g. I used "/tmp/compute-stub").

That's it. Now all you need to do is run the server application, then run the client and you'll see it works. No need to start the rmi registry.


Final notes

I did read a few comments and questions by people around the web (google :-) who seemed to have tried something similar and seemed to not have gotten it working. My guess is that the reason it didn't work was because of some other RMI related problem. Maybe the system property to tell RMI about the code base was not correctly set, maybe the Java security manager was not set up correctly.

The advice there is, make sure you have something that works with the registry first. The great tutorial that I started with takes care of all those little details.  Once I verified it worked, I made my attempt to 'eliminate' the registry.


Source Code


This is a zip containing the three sample projects. You can import them easily into Eclipse using the "Import as Existing Projects" wizard.

3 comments: