An in-depth look at RMI callbacks (4/20/99)

The JavaWorld experts answer your most pressing Java questions -- every week

Q: I have a problem with RMI callbacks. Specifically, my server program successfully invokes a client method using callbacks. However, since the client method executes on the server side in this case, how am I supposed to manipulate my frontend AWT components from this method? Or is it not possible using RMI callbacks? I need my frontend AWT components, such as Lists, to be updated with values upon callback. Please explain.

A: Here's the short answer to your question. It isn't correct to say that the client method executes on the server. When using RMI to do callbacks to an application client, the client code doesn't execute on the application server. Only the RMI proxy or stub class executes on the application's server machine. The proxy invocation then arranges for the implementation class for the callback to execute on the virtual machine of the application client. Therefore, since the RMI server logic and the frontend AWT components coexist on the same VM, there's no problem in having an RMI "callback" update information kept by AWT components.

Here's a much longer answer, with a sample program that illustrates what's going on:

It can be difficult when learning RMI to predict what's going on where. The trick is to remember that for any RMI interface there will be two classes that execute: the proxy or stub class runs in the VM of the caller, and the implementation class runs in the VM of the server.

The other thing to keep straight is that it can get confusing to label one VM the "client" and the other the "server" for a particular application. In RMI terminology, any VM that initiates an RMI invocation is the client, while any VM that receives and processes an RMI invocation is the server. I find it easier to think of Java VMs as peers, any of which may act as RMI clients or servers depending on the circumstances. Thus, in RMI there is really no such thing as a "callback" -- a callback is really just an RMI invocation in the other direction! In the context of what many people call client/server applications, it's helpful to designate the program that performs the user interface functions as the frontend or application client, and the program that provides services to that GUI as the backend or application server.

Using this new nomenclature, the question boils down to: Can RMI be used to allow a backend VM to send update information to a frontend VM and have the frontend VM use that information to update the information presented by the GUI? Put this way, the answer is: Yes!

Enough text. Let's look at this in code. Suppose we have a frontend whose mission in life is to display a list of stock symbols and stock prices The job of the backend would then be to notice when a stock's price changes and update the frontend with this information. Upon receiving such an update, the frontend would replace the old price for that stock with the new price in the AWT-based user interface.

Since you asked about updating the contents of AWT components using application server-side RMI calls, we'll use an AWT List component in the GUI. It's not the prettiest way to do it, but it does keep the code short. Apologies in advance for the flicker; this isn't a course on AWT programming!

RMI in a nutshell

Remote Method Invocation (RMI) allows Java programs to call other Java programs that execute in other virtual machines, usually across a network.

The interface to an RMI server is defined by an interface that extends the java.rmi.Remote interface. All methods defined in remote interfaces must be declared to throw the java.rmi.RemoteException.

RMI servers begin by replacing the default SecurityManager with an instance of RMISecurityManager. To advertise available services, an instance of an RMI server object is bound to a name in an RMI registry using the java.rmi.Naming.rebind method. The RMI registry is established by running the rmiregistry daemon.

An RMI server implementation must extend the java.rmi.server.UnicastRemoteObject class and must implement the remote interface for the server. Once this class is defined and compiled, the rmic program is used to automatically generate stub and skeleton classes.

RMI clients use the java.rmi.Naming.Lookup method to obtain a reference to RMI server objects.

The RMI interfaces

In any well-designed distributed system, the remote interfaces are the most important parts of the system architecture, so let's start with them. The application server provides an interface called StockInfo, which defines two method signatures: register() and unregister(). This is how the frontend client tells the StockInfo backend server that the frontend client exists.

A registered StockInfo client must implement the StockUpdate interface and must pass a reference to an object that implements the StockUpdate interface as the sole argument to the register() method. Once the StockInfo server has a reference to the StockUpdate object, the StockInfo server implementation may use the reference to talk to the frontend client at will. It is this object that acts as the "callback" object in this architecture. Here is the StockInfo interface:

package rmistock;import java.rmi.*;
public interface StockInfo extends java.rmi.Remote {
   void register(StockUpdate o) throws RemoteException;
   void unregister(StockUpdate o) throws RemoteException;
}

The StockUpdate interface looks like this:

package rmistock;import java.rmi.*;
public interface StockUpdate extends java.rmi.Remote {
   void update(String symbol, String price) throws RemoteException;
}

Once a client has registered with the StockInfo server, the server will periodically invoke the update() method of the StockUpdate object. This means that the application client is itself an RMI server and must be prepared to service RMI invocations at any time after it makes the StockInfo.register call.

The StockInfo server

Now that the RMI interfaces are defined, there have to be implementation classes for those interfaces. For the StockInfo interface, that class is called StockInfoImpl. The class has three responsibilities:

  1. To implement the StockInfo interface.
  2. To take care of the mainline chores of starting and registering an object as a server for the StockInfo interface that will name rmistock.StockInfo.
  3. To implement a background thread that obtains stock symbols and prices, and notifies each registered client of any change in price for a stock symbol. Implementing the StockInfo interface is very simple: The register() method takes the StockUpdate instance passed to it and stores it uniquely in a private Vector. The unregister() method deletes the StockUpdate instance from the Vector. Notice that these two methods are synchronized to avoid problems that result from the contention of multiple invocations trying to simultaneously access the Vector of StockUpdate instances.

Here are the implementations of the StockInfo interface methods:

public synchronized void register(StockUpdate o) throws RemoteException {
   if (!(clients.contains(o))) {
      clients.addElement(o);
      System.out.println("Registered new client " + o);
   }
}
public synchronized void unregister(StockUpdate o) throws RemoteException {
   if (clients.removeElement(o)) {
      System.out.println("Unregistered client " + o);
   } else {
      System.out.println("unregister: client " + o + "wasn't registered.");
   }
}

The StockInfoImpl server mainline-code creates an RMI registry on TCP port 5001, instantiates a StockInfoImpl object, registers that object with the RMI registry, and then creates the background stock-price update thread and starts it running:

public static void main(String args[]) {
   System.setSecurityManager(new RMISecurityManager());
   try {
      LocateRegistry.createRegistry(5001);
      StockInfoImpl sii = new StockInfoImpl();
      Naming.rebind("//:5001/rmistock.StockInfo", sii);
      System.out.println("StockInfoImpl registered and ready");
      Thread updateThread = new Thread(sii, "StockInfoUpdate");
      updateThread.start();
   } catch (Exception e) {
      e.printStackTrace();
   }
}

The price update thread implements a simple and not particularly realistic simulation of an equities market. It knows about two stocks: IBM and SUN, and simply increments their prices in lockstep every second from to 5 in quarter-point steps, dropping precipitously back to when the price hits 5. Every time the price changes, the price update thread iterates through the saved Vector of StockUpdate references. It is here that the callback is performed via the update() method:

StockUpdate client = (StockUpdate) e.nextElement();
try {
   client.update(symbol, "" + price);
} catch (RemoteException ex) {
   try {
      unregister(client);
   } catch (RemoteException rex) {
   }
}

The code that executes here in the backend's VM isn't the actual implementation of the StockUpdate interface's update() method, but rather a proxy method that serializes the symbol and price strings back across to the frontend VM. On the frontend side, those serialized strings are turned back into Java objects and then passed to the implementation method, which then executes on the frontend's VM. We'll see what that implementation does when we step through the frontend code below.

Look at the code in the catch clause -- this is triggered when a frontend VM terminates. The update() method throws a java.rmi.RemoteException. This catch clause uses the exception as an opportunity to unregister the client so that future iterations of the price update thread don't bother to try to contact that application client again. The try/catch around the unregister calls is just for form's sake: unregister is really supposed to be invoked remotely, but here we use it locally so there's no possibility of a RemoteException being raised.

The StockUpdate GUI client

The frontend component of this system is implemented in two classes: StockWatcherGUI and StockList.

StockWatcherGUI handles the mainline code for the GUI -- it sets up an AWT frame, and inserts a StockList object into the frame. It also sets up the frontend VM as an RMI server and implementation for the StockUpdate interface. Since the frontend always passes a reference to the StockUpdate-implementing object directly to the backend as an argument to the register() method, there is no need to register the StockUpdate instance in the RMI registry -- no one ever has to look them up. The mainline code, however, does look up the instance reference bound to the rmistock.StockInfo name in the RMI registry for later use. The implementation of the StockUpdate interface is the update() method of this StockWatcherGUI class.

public synchronized void update(String symbol, String price) throws RemoteException {
   l.updateStock(symbol, price); // l is an AWT component
}

Notice again that it is synchronized -- even though in this system it is unlikely that we'll get more than one update call coming in at once, that's not guaranteed to be the interface definition, and a good RMI server must be ready to operate in a multithreaded environment. This method will execute in the frontend's VM, and it has access to the AWT components of the StockWatcherGUI. Once we're in this method, it's child's play to update the AWT component. In fact, all this method does is forward the call to our StockList AWT component via the StockList's updateStock method. The StockList component is a subclass of java.awt.List, so a StockList object is an AWT component. The updateStock method directly modifies the string elements displayed in the list box, formatting them with the incoming stock symbols and prices.

public synchronized void updateStock(String symbol, String price) {
   int i = 0;
   for (i = 0; i < stocks.length; i++) {
      if (stocks[i].equals(symbol)) break;
   }
   if (i >= stocks.length) return;
   replaceItem(formatStock(symbol, price), i);
}

That's pretty much all there is to it. The complete code, along with build and run scripts for Windows is at jw-05-javaqa.zip. You have to unpack the zip file and then run the scripts from the RMI subdirectory. Figure 1 shows a diagram of where each of the objects exist at runtime and how the invocations flow between each of these objects in this RMI-based system.

Figure 1. Objects and interactions in the rmistock example

Just to prove to yourself that callback code actually executes on the front end, load the class files on two separate machine. On the machine that will be the back end, delete the StockWatcherGUI.class and StockWatcherGUI_Skel.class files so that the server doesn't have access to them at all. All it needs is the StockWatcherGUI_Stub.class file -- this is the proxy class itself that was generated by rmic.

Random Walk Computing is the largest Java/CORBA consulting boutique in New York, focusing on solutions for the financial enterprise. Known for their leading-edge Java expertise, Random Walk consultants publish and speak about Java in some of the most respected forums in the world.

Learn more about this topic

  • Download the complete code, along with build and run scripts for Windows. You'll have to unpack the zip file and then run the scripts from the RMI subdirectory. http://www.javaworld.com/jw-05-1999/javaqa/jw-05-javaqa.zip
  • "Increase the functionality in your distributed client/server apps," by Michael Shoffner (JavaWorld, October 1997). Step by Step columnist Michael Shoffner provides a good introduction to RMI http://www.javaworld.com/javaworld/jw-10-1997/jw-10-step.html
  • "Networking our whiteboard with Java 1.1," by Merlin Hughes (JavaWorld, December 1997). Step by Step columnist Merlin Hughes delivers another perspective on RMI, including a design pattern that helps it all seem more automatic http://www.javaworld.com/javaworld/jw-12-1997/jw-12-step.html
  • Sun's RMI Web page offers basic and detailed information on RMI http://java.sun.com/products/jdk/rmi/
Join the discussion
Be the first to comment on this article. Our Commenting Policies