Sun Java Solaris Communities My SDN Account Join SDN
 
Technical Articles and Tips

The Lifecycle of an RMI Server and Dynamic Class Loading in RMI

 

Tech Tips Archive


WELCOME to the Java Developer Connection sm(JDC) Tech Tips, February 27, 2001. This issue covers:

This tip was developed using Java 2 SDK, Standard Edition, v 1.3.

This issue of the JDC Tech Tips is written by Stuart Halloway, a Java specialist at DevelopMentor (http://www.develop.com/java).

Pixel

The Lifecycle of an RMI Server

RMI allows you to invoke methods on objects in other Java virtual machines*, often across the network. Applications that use RMI to invoke methods in these remote objects are typically composed of two separate programs: an RMI client that makes requests, and an RMI server that executes the requests and returns results to the client. This tip examines the steps in the operation of an RMI server, that is, it's lifecycle.

All RMI servers implement a remote interface, that is an interface that extends the interface java.rmi.Remote. A remote interface is the protocol that is used to communicate a request to an RMI server and to return results. So let's create a simple remote interface:

import java.rmi.*;
public interface Echo extends Remote {
  public String echo(String value) throws
    RemoteException;
}

Because echo is a method in a remote interface it must be declared to throw a RemoteException.

An object that implements a remote interface becomes a remote object, and its methods can be invoked remotely (that is, in other VMs) through RMI. Let's write a simple RMI server that implements the remote interface:

import java.io.*;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
public class EchoServer implements Echo {
  public String echo(String value) {
    return value;
  }
  public static void main(String[] args) {
    try {
    
      //create an object
      EchoServer serv = new EchoServer();
      
      //export the object 
      Echo remoteObj = 
              (Echo)
                UnicastRemoteObject.exportObject(serv);
      
      //make object findable 
      Registry r = LocateRegistry.getRegistry(
                                         "localhost",
                                           1099);      
      r.rebind("ECHO", remoteObj);
      
      BufferedReader rdr = new BufferedReader(
                               new InputStreamReader(
                                 System.in));

      //Serve clients
      while (true) {
        System.out.println(
          "Type EXIT to shutdown the server.");
        if ("EXIT".equals(rdr.readLine())) {
          break;
        }
      }
      
      //unregister object
      r.unbind("ECHO");
      
      //unexport object
      UnicastRemoteObject.unexportObject(serv, true);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

The EchoServer class extends RemoteObject, which provides implementations of hashCode, equals, and toString for remote objects. However, EchoServer is deliberately designed not to extend UnicastRemoteObject. The UnicastRemoteObject class is a subclass of RemoteObject that hides most of the details of making an object available to remote clients. For the purpose of demonstration, EchoServer instead calls RMI APIs that show each step of a server's lifecycle, as follows:

  1. Create an object:
       EchoServer serv = new EchoServer();
       
  2. Export an object (declare intent to remote):
         Echo remoteObj = 
                      (Echo)
                        UnicastRemoteObject.exportObject(
                          serv);
       
  3. Make the object findable (in this case by binding into the registry):
         Registry r =
           LocateRegistry.getRegistry("localhost", 1099);
       
  4. Serve clients:
         while (true) {
            System.out.println("Type
              EXIT to shutdown the server.");
            if ("EXIT".equals(rdr.readLine())) {
              break;
            }
         }     
       

  5. Unregister the object if registered:
         r.unbind("ECHO");
       
  6. Unexport the object:
         UnicastRemoteObject.unexportObject(serv, true);   
       

A seventh step in an RMI server's lifecycle (though not demonstrated in the EchoServer code) is the garbage collector collecting the object if the object becomes unreferenced.

Step 1 is common to any Java object. Step 4 is common to any Java object that needs to serve a client request. Also garbage collection is common to Java objects that become unreferenced. But the other steps are specific to remote objects.

In Step 2, you explicitly export an object. This tells the RMI runtime that you want this object to be available to remote VMs. Exporting an object also returns the stub for the object. The stub is a class that does the work of formatting and transmitting method arguments to the RMI server and returning results to the RMI client. Later in this tip, you'll see how to create the stub. Note that many RMI classes skip this step of explicitly exporting an object. Instead these classes subclass UnicastRemoteObject, which automatically exports the object in its constructor.

In Step 3, you make an object findable by code running in other VMs. The simplest way to do this is to register the stub with the RMI registry. The registry a simple name-object lookup service that listens on port 1099, by default. Other ways to make an object findable are to simply pass the object to a remote method, or return it in a remote method implementation. In these situations, the RMI runtime automatically replaces the object with its stub. Of course, from the RMI client's perspective, this leads to an interesting question: Where did the remote object come from? Which leads to the interesting answer: It came from another remote object! Sooner or later, there must be one or more "original" objects that are obtained without the help of other RMI objects. The RMI registry provides this bootstrap service.

In Step 4, you decide how long the object will be available to service clients. In the simple example above, the server process presents a console prompt that tells the user how to shut down the server ("Type EXIT to shutdown the server."). In real-world deployments, you need to use application-specific logic to decide when to stop exporting your server.

Steps 5 and 6 simply reverse steps 3 and 2.

Now let's test this simple RMI server. To do that, you need to performs the following actions:

  • Create the stub class.
  • Create an RMI client that connects to the RMI server.
  • Start the RMI registry.
  • Run the RMI server.
  • Run the RMI client.

Create the stub class with the rmic command-line tool:

rmic EchoServer

If you look in your class file directory, you should see the EchoServer_stub.class file.

Use the following code for the RMI client that connects to and uses EchoServer:

import java.rmi.*;
public class EchoClient {
  public static void main(String [] args) {
    try {
      System.out.println("Connecting to echo
        server...");
      Echo e = (Echo) Naming.lookup("ECHO");
      String result = e.echo("Hello");
      System.out.println("Echo returned " + result);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}


You will need three console windows to start the RMI registry, run the RMI server, and run the RMI client. In all of these windows, make sure your class path is set to the directory where the various Echo classes live. Then, run the following three commands, one per window, in this order:

  rmiregistry
  java EchoServer
  java EchoClient

The client should successfully connect to the server and return an echo. You should see the following displayed:

  Connecting to echo server...
  Echo returned Hello

If it does not, read on. The tip "Dynamic Class Loading in RMI" shows some techniques for debugging RMI problems.

For more advanced ideas on the RMI server lifecycle, read the Remote Object Activation tutorial at http://java.sun.com/j2se/1.3/docs/guide/rmi/activation.html

Dynamic Class Loading in RMI

The example in the tip "The Lifecycle of an RMI Server" is a bit unrealistic because the client and server code reside in the same directory or folder. In a typical deployment, the RMI client, EchoClient, would be on one machine, and the RMI server, EchoServer, would be on another. What about the Echo interface? In most systems, the interface is needed on both client and server. However, this is not typically a deployment problem because interfaces rarely change. Besides, the EchoClient could not do much with an EchoServer without knowing some interface through which to make the call.

The stub presents more of a deployment problem. In JDK 1.3, RMI requires that clients use a stub class to connect to a remote VM. The stub class hides the details of network communication, sending method arguments and waiting for a return value. Today, you generate stub classes with the rmic command line tool. However future versions of RMI might allow clients to build stubs dynamically at runtime using dynamic proxies. Until that feature is added, clients need a way to guarantee that stubs are available. Installing the stubs on the client is not always viable, because stubs are an implementation detail that might change over time. Dynamic class loading allows client virtual machines to find stubs at runtime, without any special coding in the client.

To see the problem, test the EchoClient and EchoServer, but unlike the previous tip, run the RMI registry, EchoClient, and EchoServer in separate class paths. This would be more typical of a real deployment situation.

First, start the RMI registry from a console that does not have any of the Echo classes on the class path. Pass in an additional argument to debug class loading problems, as shown below:

  rmiregistry -J-Dsun.rmi.loader.logLevel=VERBOSE

Now move all the server code (EchoServer, EchoServer_stub, and Echo) into a server subdirectory or subfolder. Then try to start EchoServer from its parent directory or folder, as follows:

  java -cp server EchoServer

You should see an exception:

  java.rmi.ServerException: RemoteException
    occurred in server ...
        java.rmi.UnmarshalException: error
          unmarshalling ...
        java.lang.ClassNotFoundException:
          EchoServer_Stub ...

This exception, received by the server, reports an error that actually fist occurred in the rmiregistry process. When the server attempts to bind the name into the registry, the registry must be able to load the stub class. The debug flag on the command line causes the registry to log this process. So in the rmiregistry console, you see something like this:

   ... loading class "EchoSever_Stub" from []

The [] indicates that the registry does not know where to look for the stub. To fix this problem, the server needs to tell clients where to find any needed classes. To do this, you can specify a codebase on the command line to the server process:

java -cp server \
 -Djava.rmi.server.codebase=file:{serverloc}/ EchoServer

Replace the codebase value (that is file:{serverloc}) with the URL to your server, and do not omit the trailing '/' and space. For example, if the full URL to your server is file:/myhome/server, enter the following command:

java -cp server \
 -Djava.rmi.server.codebase=file:/myhome/server/ EchoServer

When you specify a codebase, the server annotates all outbound objects with URL location information. Clients can use this location information to download classes when necessary. If you try this command, the server should function normally, that is, you should see the following prompt on the console line:

  Type EXIT to shutdown the server. 

In the rmiregistry output you should also see a line proving that the registry loaded your code from the codebase:

  ... loading ... from [file:/yourURL/] 

Now copy the EchoClient class and the Echo class to another subdirectory or subfolder called client. Then run EchoClient from its parent directory or folder, passing the same debugging flag that you did to the rmiregistry:

  java -cp client -Dsun.rmi.loader.logLevel=VERBOSE EchoClient

You will get an exception telling you that the RMI class loader is disabled.

To fix this problem, you need to use dynamic class loading. However, to use dynamic class loading, you must install a security manager. Without a security manager installed, servers could easily attack you by sending malicious code that masquerades as a remote stub. When you turn on security, you also need a policy file that gives you the permissions necessary to do RMI work. So save the following policy file as SimpleRMI.policy in the client folder:

 grant {
    permission java.net.SocketPermission" 
                        *:1024-", "accept, connect";
    permission java.io.FilePermission 
           "${/}thisproject${/}server${/}-", "read";
  };  

The SocketPermission lets RMI use sockets on nonprivileged ports, and the FilePermission allows classes to be dynamically downloaded from file URLs. Make sure you change "thisproject" to the location you are using, for example:

 grant {
    permission java.net.SocketPermission" 
                        *:1024-", "accept, connect";
    permission java.io.FilePermission 
                "${/}myhome${/}server${/}-", "read";
  };   

Notice the use of ${/} instead of / or \. The policy expands ${/} to the correct path or folder delimiter on your host platform.

Now try the client with security installed, and with class load tracing enabled:

java -cp client -Dsun.rmi.loader.logLevel=VERBOSE \
     -Djava.security.policy=client/SimpleRMI.policy \
     -Djava.security.manager EchoClient

This time the client works as expected. The log output shows that the client successfully downloaded the stub from the server folder.

  ... loading class "EchoServer_Stub" from
[file:/yourpath/]
  Echo returned Hello

This is very powerful. After the class is annotated with the codebase, the codebase travels with the class without any extra effort. You could pass EchoServer from machine to machine forever, and each machine would know where to find the stub -- in fact, the machines would know where to find any class they needed.

But there's something to watch out for, something that you can observe by doing the following: Making sure that all console processes are shut down, restart the rmiregistry. But this time restart it with the server classes on its class path. (The easiest way to do this is to make sure that the class path is not set and then run rmiregistry from the server directory or folder.) Start the server as before, passing in the codebase annotation. Now if you try to run the client from the client folder, class loading fails:

java -cp client -Dsun.rmi.loader.logLevel=VERBOSE \
     -Djava.security.policy=client/SimpleRMI.policy \
     -Djava.security.manager EchoClient
... loading class "EchoServer_Stub" from [] 
java.rmi.UnrmarshalException: ...
...

This can be the most baffling problem that a novice RMI programmer faces. Everything looks normal. The codebase is set correctly in the server, and it appears that rmiregistry loaded the stub from the correct codebase. But somehow the codebase annotation gets lost, and the client cannot find the stub.

In order for the stub's annotation to flow from one process to another, intermediate processes must either (1) implicitly load the annotated class using an RMI-created class loader or (2) explicitly reset the codebase. If any intermediate VM finds the stub on its class path, the normal system class loader is used, and the annotation passed from the server is lost. The series of events leading to this failure is:

  1. The server explicitly sets codebase annotation from the command line.
  2. The server binds the stub, which picks up the annotation.
  3. The registry process attempts to load the stub, and finds it on the class path.
  4. Because the stub is on the class path, the annotation is lost.
  5. The client attempts to look up the stub, but has no codebase to find it.

You could work around this problem by resetting the codebase on every intermediate virtual machine, or even on the end client. This is rarely appropriate. The server is the logical owner of the stub, and should tell clients where to find it. In order to leave the server in charge, set the codebase on the server, and make sure that client processes never place dynamically-loaded classes on their class paths.

This tip demonstrates that RMI problems are much easier to troubleshoot if you use the correct debugging flags. In this example, the sun.rmi.loader.logLevel flag makes it easy to determine where in the system the annotations are being lost. Another useful flag is java.rmi.server.logCalls=TRUE. This flag logs all remote calls. For a more complete list of RMI debugging flags, see http://java.sun.com/j2se/1.3/docs/guide/rmi/faq.html#properties. Note that the properties that begin with "rmi" are part of the public specification. Properties that begin with "sun" are subject to change or removal in future versions of the implementation.

— Note —

Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun MicrosystemsTM purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), uncheck the appropriate checkbox, and click the Update button.

— Subscribe —

To subscribe to a JDC newsletter mailing list, go to the Subscriptions page (https://softwarereg.sun.com/registration/developer/en_US/subscriptions), choose the newsletters you want to subscribe to, and click Update.

— Feedback —

Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com

— Archives —

You'll find the JDC Tech Tips archives at:

http://java.sun.com/jdc/TechTips/index.html

— Copyright —

Copyright 2001 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:

http://java.sun.com/jdc/copyright.html

—LINKS TO NON-SUN SITES—

The JDC Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource.

JDC Tech Tips February 27, 2001

* As used in this document, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.

Sun, Sun Microsystems, Java, Java Developer Connection, and Java Remote Method Invocation are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.