ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Learning Command Objects and RMI

by William Grosso, author of Java RMI
10/17/2001

This is the first article in a three-article series.

In this article, I introduce the basic ideas behind command objects. In order to do so, I drag in an example application that provides a translation service from a remote server. After introducing this application, I will show how to use command objects to structure the remote method invocations (RMI) made from a client program. As part of this article, I will introduce a fairly general framework for encapsulating remote method calls in command objects.

At the outset, you should know that these articles require a fair amount of RMI knowledge and experience. If you're not familiar with RMI, my book Java RMI is a pretty good place to start.

An Overview of the Example Application

This series of articles concerns a fairly advanced idea in distributed programming. In order to do justice to the concepts, we're going to need an example that's somewhat more elaborate than the distributed programming version of "Hello World." However, building a full-scale complex application with transactional logic and a persistent storage mechanism is also inappropriate. For these reasons, this series of articles focuses on a simple translation service.

A translator is an RMI server that implements the following interface:

import java.rmi.*;

public interface Translator extends Remote, Constants {
  public boolean canTranslate(Language sourceLanguage, Language targetLanguage)
    throws RemoteException;
  public Word translate(Word wordToTranslate, Language targetLanguage)
    throws RemoteException, CouldNotTranslateException;
}

Related Reading

Java RMIJava RMI
By William Grosso
Table of Contents
Index
Sample Chapter
Full Description

That is, a translator has to implement two methods. The first method describes the capabilities of a particular translator and allows a client program to figure out whether it is an appropriate translator for a particular request (or series of requests). The second method actually performs the translation.

Note that this relies on two serializable classes, Word and Language, and a set of global parameters defined in the Constants interface. Language is simply a class that defines a type-safe enumeration of the languages currently supported -- the idea is that we have a predefined list of languages, instead of relying on strings or integer constants. Instances of Word are data structures that hold both a string (the word to be translated) and a language (after all, the string "chat" means very different things depending on whether it is the text of a word in French or the text of a word in English).

You can look at the source code for both Language and Word by downloading the source code for this series.

Download all of the example files discussed in this article.

Comment on this articleWhat's your experience with command objects? If you're learning, send William your questions and feedback.
Post your comments

Caution! This series of articles is about using command objects to simplify the client side of RMI-based applications. In order to write it, I needed to provide places to insert command objects. So I wrote (and you can download) the rest of the code, including the client GUI and a very primitive set of servers. The translator application runs and can easily be adapted to other uses. But it's not completely implemented, it's not bulletproof, and it's not intended to form the basis of your enterprise-grade-serves-ten-thousand-users client-server application.

Our application is built around the following assumptions: there are a set of naming services (in our case, instances of the RMI registry) out there, running on some set of server machines. Many instances of Translator have been bound into these naming services and the job of the client application is to find the right translator and then translate some words.

Linguistically, this isn't very realistic. Generally speaking, translation at the level of single words is not very accurate. But if we ignore those sorts of quibbles (and recognize that simple translation services can be useful), we wind up with an fairly prototypical application structure for networked services. Namely:

The following diagram illustrates the application topology.

Diagram.

The Servers

This series is about using command objects to simplify the client side of an RMI application. The servers, in particular, don't need to be well-implemented in order to have things work. Thus, I've left most functionality "stubbed out" in them.

In particular, we have one class, Launcher, which does the following:

Here's the entire source code for Launcher:

import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;

public class Launcher implements Constants {
  public static void main (String[] args) {
    launchRegistryIfNecessary();
    try {
      bind (new EnglishToFrenchTranslator(), EnglishToFrenchTranslator.NAME);
    }
    catch (RemoteException errorInLaunchingEToFServer) {
      System.out.println("Error in launching translation server : " + EnglishToFrenchTranslator.NAME);
      errorInLaunchingEToFServer.printStackTrace();      
    }
    try {
      bind (new EnglishToSpanishTranslator(), EnglishToSpanishTranslator.NAME);
    }
    catch (RemoteException errorInLaunchingEToSServer) {
      System.out.println("Error in launching translation server : " + EnglishToSpanishTranslator.NAME);
      errorInLaunchingEToSServer.printStackTrace();      
    }
  }

  private static void bind(Remote server, String name) throws RemoteException {
    Registry registry = LocateRegistry.getRegistry(TRANSLATOR_REGISTRY_PORT);
    registry.rebind(name, server);
  }

  private static void launchRegistryIfNecessary() {
    try {
      LocateRegistry.createRegistry(TRANSLATOR_REGISTRY_PORT);
    }
    catch (RemoteException ignored)  {}
  }
}

The actual translators themselves are not very sophisticated servers. Here, for example, is the source for EnglishToFrenchTranslator:

import java.rmi.*;
import java.rmi.server.*;

public class EnglishToFrenchTranslator extends UnicastRemoteObject implements Translator, Constants {
  public static final String NAME = "Basic English To French";

  private Word _universalResponse = new Word("bonjour", Language.getLanguage(FRENCH));
  public EnglishToFrenchTranslator () throws RemoteException {
  }
  
  public boolean canTranslate(Language sourceLanguage, Language targetLanguage)
    throws RemoteException
  {
    if (!sourceLanguage.equals(Language.getLanguage(ENGLISH))) {
      return false;
    }
    return targetLanguage.equals(Language.getLanguage(FRENCH));
  }

  public Word translate(Word wordToTranslate, Language targetLanguage)
    throws RemoteException, CouldNotTranslateException
  {
    Language sourceLanguage = wordToTranslate.getLanguage();
    if (!canTranslate(sourceLanguage, targetLanguage)) {
      throw new CouldNotTranslateException(NAME + " can not translate from " + sourceLanguage + " to " + targetLanguage) ;
    }
    return _universalResponse;
  }
}

It's not very hard to implement this class more fully (for example, by using one of the English-to-French vocabularies publicly available on the Internet), but it's also not relevant for these articles.

Note: Throughout this series of articles, the examples of "client code" involve a GUI and an end user. But everything I say in this article applies equally well if we interpret "client side" to mean "program that is making an RMI call to another program." That is, any program that is behaving like a client (e.g., is the program making a remote method invocation to another program).

The Client

The GUI part of the client application is very simple, as well. It provides a way for the user to enter a word, a way for the user to (manually) choose a translation service, and a way for the user to actually get a word translated. Here's what it looks like in action:

Screen shot.

This interface consists of two elements for entering data, a button for the user to make a request, and a panel which displays the results. Here's how it's used.

  1. The user uses the top panel to select which translator to use. In order to do this, the user enters the name of a machine which is running an RMI registry and the port on which the registry is running. The combo box will then automatically be populated with the names of all the instances of Translator that are bound into the specified registry. After this, the user must manually choose a translator.

    The first panel is actually an instance of TranslatorPanel (a simple class which enables the user to enter the information necessary to get a Translator; it's contained in the downloadable source code).

    The first panel.

  2. The user then uses the second panel to enter the text of a word and to choose the appropriate language. The second panel is actually an instance of WordPanel (a simple class which enables the user to enter a word; it's contained in the downloadable source code).

    The second panel

  3. After all the data is entered, the user clicks the Translate Word Now button.

A First Pass at Implementing the Remote Calls

This client application, simple as it is, involves two distinct sets of remote calls. The first is during step (1) above, when the combo box is automatically populated with the names of the appropriate servers. In order to do this, the client application must actually query the specified RMI registry and find the names of all the translators which have been bound into the registry.The second set of remote calls occurs when the user clicks on the Translate Word Now button.

In both of these cases, the remote method invocations occur as a result of a user action, and are placed inside an event handler. This is fairly typical for simple client applications -- the user does something and the program responds, usually by sending a message to a server somewhere.

Let's take a look at what's involved in implementing the remote call that occurs when the user clicks the Translate Word Now button. The client program:

  1. Must get the information about the server from the top panel.
  2. Must get both the word to be translated and the target language from the second panel.
  3. Must make the remote method invocation.

Fortunately, the first two steps are easy; they're just calls to the appropriate methods on TranslatorPanel and WordPanel. And RMI makes the rest of a simple implementation very easy indeed. Here's the code for the first pass at an ActionListener attached to the Translate Word Now button (this code comes from the ClientFrame_FirstPass class).

  private class TranslationListener implements ActionListener
    public void actionPerformed(ActionEvent actionEvent) {
      String resultText = "";
      Translator translator = _translatorPanel.getTranslator();
      Word word = _wordPanel.getWord();
      Language targetLanguage = _translatorPanel.getTargetLanguage();

      try {
        Word result = (Word) translator.translate(word, targetLanguage);
        resultText = result.toString();
      }
      catch (Exception anyException) {
        resultText = anyException.toString();
      }
      finally {
        _resultsPanel.setText(resultText);
      }
    }
  }

This encapsulates the remote call in a try/catch/finally block and deals with the exceptions that can be thrown by simply reporting them back to the user. This will work for simple client-side applications (for simple server-to-server applications, the exception should probably be logged instead).

The only problem with this code is that it completely ignores the possibility of transient network or server failures. In practice, remote calls frequently fail for any number of reasons. The network could be congested, the call could time out because the server is busy, the server could have been restarted in the time between when the stub was obtained and when the call was made, and so on. Partial failure, where one or more components of a distributed system go down or become temporarily inaccessible while the other components continue to function, is a fact of life in distributed programming.

The designers of RMI attempted to force programmers to deal with this reality by requiring every remote method signature to declare that it can throw RemoteException. The semantics of RemoteException are simple: an instance of RemoteException means something went wrong, somewhere in the infrastructure. For example, the call might never have made it to the server, or the server might have received the call but crashed before returning a value, or the server might have returned a value (e.g., the server is done), but the value never made it back to the client because of a network error. All of these problems, which the client couldn't possibly distinguish between, will cause an instance of RemoteException to be thrown.

Network failures are a fact of life, as are server crashes. However, it's not so bleak: another fact of life is that while networks occasionally fail, they also frequently recover (e.g., network failures are often short-lived). And while servers do crash, they are often restarted.

This discussion leads to the following realization: what we need is a slightly more robust idea of what it means to make a remote method call. If a remote method call fails for an unknown reason, the application should probably try again. Most programmers eventually wind up using what I call the "try three times and punt" model. Doing this involves surrounding the remote method call with a loop that retries the call if an instance of RemoteException was thrown.

  private class TranslationListener implements ActionListener
    public void actionPerformed(ActionEvent actionEvent) {
      int numberOfTries = 0;
      String resultText = "";
      Translator translator;
      Word word = _wordPanel.getWord();
      Language targetLanguage = _translatorPanel.getTargetLanguage();
      while (numberOfTries < MAXIMIMUM_NUMBER_OF_TRIES) {
        numberOfTries++;
        translator = _translatorPanel.getTranslator();

        try {
          Word result = (Word) translator.translate(word,
            targetLanguage);
          resultText = result.toString();

          break;
        }
        catch (CouldNotTranslateException exceptionThrownByServer) {
          resultText = exceptionThrownByServer.toString();

          break;
        }
        catch (RemoteException exceptionThrownByRMIInfrastructure) {
          resultText = exceptionThrownByRMIInfrastructure.toString();

          try {
            Thread.sleep(REMOTE_CALL_RETRY_DELAY);
          }
          catch (InterruptedException ignored) {}
        }
        catch (Exception uncheckedExceptionWeShouldStillCatch) {
          resultText = uncheckedExceptionWeShouldStillCatch.toString();

          ExceptionLog.reportException(uncheckedExceptionWeShouldStillCatch);
          break;
        }
      }
      _resultsPanel.setText(resultText);
    }

This version of our listener has several notable features. The first is that it distinguishes between three distinct types of exceptions. If an instance of RemoteException is thrown, the code tries again. If an instance of an expected exception (e.g. one declared in the signature of the remote method) is thrown, we assume that the server has already logged the error, and all we need to do is inform the user. If an unexpected exception (for example, a NullPointerException) is thrown, the client code also tries to log the exception somehow. (If the client is truly an end-user application, logging the error is problematic and probably involves e-mail. If the client is another server on a controlled machine, logging to a file is often sufficient).

Another important point is that we invoke _translatorPanel.getTranslator(); during each retry, to refetch the stub. We do this in case the server went down and was restarted. If the server was restarted, our old stub will not work. Hence, we probably don't want to try to connect to the server using it again.

A third noticeable feature is that we've inserted a call to Thread.sleep in the middle of the retry logic. If something is wrong, waiting a little bit before retrying is a sound strategy. In fact, it's usually good practice to increase the delay with each retry. For example, waiting five seconds, then 10 seconds, then 15 seconds. You might also want to customize this based on the client connection -- if the client is making the call over the Internet from a dialup modem, you might want a longer delay. (Or you might want to just give up; if the modem dialup connection is flaky, you might not want to try multiple times)

The final thing to notice is that this is a lot of code surrounding the remote method call. A simple 10 line try/catch/finally block has blossomed into a fairly complex 26-line while loop involving a pair of external constants. That's a lot of code for each remote method call in your client application!

There's Another Problem Here

Beyond the sheer density of the above code, this solution creates other problems. RMI tries very hard to make remote method calls look a lot like ordinary method calls. This is convenient, and makes RMI very easy to use. But it also leads to mental lapses on the part of programmers -- they slip, and forget that the method call is really a remote call. Consider the first version of the remote call code.

  private class TranslationListener implements ActionListener
    public void actionPerformed(ActionEvent actionEvent) {
      String resultText = "";
      Translator translator = _translatorPanel.getTranslator();
      Word word = _wordPanel.getWord();
      Language targetLanguage = _translatorPanel.getTargetLanguage();

      try {
        Word result = (Word) translator.translate(word, targetLanguage);
        resultText = result.toString();
      }
      catch (Exception anyException) {
        resultText = anyException.toString();
      }
      finally {
        _resultsPanel.setText(resultText);
      }
    }

  }

A programmer who reads the above code has absolutely no way to deduce from the code that a remote method call is involved (and, hence, a programmer reading through the source code three months later probably won't suspect that a retry loop is necessary). This means that if there are 37 places in your code that involve remote calls, you can expect a distribution roughly like the following:

That is, you can expect lots of code redundancy and more than a few places where the code either isn't very good or isn't correct.

Command Objects

The AbstractRemoteMethodCall and RetryStrategy classes

The solution to the problems outlined in the previous section is to implement, and consistently use, command objects. A command object is, quite simply, an object that encapsulates a method call. That is, to the object which is using the command object, it's simply a fancy method invocation. For example, our code will eventually replace the following three lines from our first pass at writing the code:

try {
  Word result = (Word) translator.translate(word, targetLanguage);
  resultText = result.toString();
}

Here's the new code:

TranslateWord translateMethod = new TranslateWord(translator, word, targetLanguage);
try {
  Word result = (Word) translateMethod.makeCall();
  resultText = result.toString();
}

This new code simply adds a level of indirection by creating an instance of the TranslateWord command object (which encapsulates the original method call) and then invoking its makeCall method.

Why would we do such a thing ? Well, Design Patterns (Gamma, etc., Addison-Wesley, 1995) gives the following motivation for the command object pattern:

Intent. Encapsulates a request as an object, thereby letting you parametrize clients with different requests, queue or log requests, and support undoable operations.

These are all really good reasons for using the comand object pattern: they're compelling, easy to understand, and apply to a wide range of programs. (Every program with a graphical user interface should support some notion of "undo." Command objects are the canonical way to do so.)

In effect, we're adding a fourth reason, which really only applies to distributed programming, to the above list. Namely, you might also want to use command objects if the request might fail for transient reasons. In such cases, it's easy to implement retry logic as part a generic base class.

Note: In the second article in this series, we'll add a fifth reason (to refresh or resynchronize local caches of information acquired from distinct processes before processing a request).

The solution I'm going to outline here is one I've used on several projects in order to wrap remote method calls used in server-to-server communication. It involves one abstract base class, AbstractRemoteMethodCall, one abstract convenience class, RetryStrategy, and two concrete subclasses of RetryStrategy. These classes play the following roles.

Here's the code for RetryStrategy and AdditiveWaitRetryStrategy.

public abstract class RetryStrategy {
  public static final int DEFAULT_NUMBER_OF_RETRIES = 3;
  private int _numberOfTriesLeft;

  public RetryStrategy() {
    this(DEFAULT_NUMBER_OF_RETRIES);
  }

  public RetryStrategy(int numberOfRetries){
    _numberOfTriesLeft = numberOfRetries;
  }

  public boolean shouldRetry() {
    return (0 < _numberOfTriesLeft);

  }

  public void remoteExceptionOccured() throws RetryException {
    _numberOfTriesLeft --;
    if (!shouldRetry()) {
      throw new RetryException();
    }
    waitUntilNextTry();
  }

  protected abstract long getTimeToWait();

  private void waitUntilNextTry() {
    long timeToWait = getTimeToWait();

    try {
      Thread.sleep(timeToWait );
    }
    catch (InterruptedException ignored) {}
  }
}

public class AdditiveWaitRetryStrategy extends RetryStrategy {
  public static final long STARTING_WAIT_TIME = 3000;

  public static final long WAIT_TIME_INCREMENT = 5000;


  private long _currentTimeToWait;
  private long _waitTimeIncrement;
  
  public AdditiveWaitRetryStrategy () {
    this(DEFAULT_NUMBER_OF_RETRIES , STARTING_WAIT_TIME, WAIT_TIME_INCREMENT);
  }

  public AdditiveWaitRetryStrategy (int numberOfRetries, long startingWaitTime, long waitTimeIncrement) {
    super(numberOfRetries);
    _currentTimeToWait = startingWaitTime;

    _waitTimeIncrement = waitTimeIncrement;

  }

  protected long getTimeToWait() {
    long returnValue = _currentTimeToWait;

    _currentTimeToWait += _waitTimeIncrement;

    return returnValue;
  }
}

The version of AbstractRemoteMethodCall used in this article is only slightly more complex than the code involved in RetryStrategy . Here it is:

import java.rmi.*;
import java.rmi.server.*;

public abstract class AbstractRemoteMethodCall
{
  public Object makeCall() throws ServerUnavailable, Exception {
    RetryStrategy strategy = getRetryStrategy();
    while (strategy.shouldRetry()) {
      Remote remoteObject = getRemoteObject();
      if (null==remoteObject) {
        throw new ServerUnavailable();
      }
      try {
        return performRemoteCall(remoteObject);
      }
      catch (RemoteException remoteException)
      {
        try {
          strategy.remoteExceptionOccured();
        }
        catch (RetryException retryException) {
          handleRetryException(remoteObject);
        }
      }
    }  
    return null;
  }

  protected abstract Remote getRemoteObject() throws ServerUnavailable;

  protected abstract Object performRemoteCall(Remote remoteObject) throws RemoteException, Exception;


  protected RetryStrategy getRetryStrategy() {
    return new AdditiveWaitRetryStrategy();
  }

  protected void handleRetryException(Remote remoteObject) throws ServerUnavailable {
    ExceptionLog.reportException("Repeated attempts to communicate with " + remoteObject + " failed.");
    throw new ServerUnavailable();
  }
}

Essentially, the makeCall method in AbstractRemoteMethodCall embodies the loop we wrote in section two, using four template methods to allow the code to be customized for a particular remote call. This is a fairly clean and straightforward use of the command object pattern.

There is one fly in the ointment: we've lost a fair amount of type information. makeCall is declared as returning instances of Object and throwing instances of Exception. There's no way around the fact that this is ugly -- it means our code will have to perform a cast on the value returned from makeCall and will use a try/catch block that catches instances of Exception. On the bright side, however, catch blocks rely on run-time type information rather than the statically declared exception types. Thus, for example, if we write code like this:

try {
  Word result = (Word) translateMethod.makeCall();
  resultText = result.toString();
}
catch (CouldNotTranslateException couldNotTranslateException) {
  resultText = COULD_NOT_TRANSLATE_STRING;
}
catch (Exception e) {
  resultText = e.toString();
}
finally {
  _resultsPanel.setText(resultText);
}

then if the server throws an instance of CouldNotTranslateException, _resultTextwill be set to COULD_NOT_TRANSLATE_STRING (even though makeCall isn't declared as throwing CouldNotTranslateException, the run-time type of the exception determines which catch block is actually used).

Implementing our Translate Calls Using Method Objects

Now that we have our command object framework in place, let's revisit the translate code. Here's the new version of the ActionListener attached to the Translate Word Now button.

private class TranslationListener implements ActionListener {
  public void actionPerformed(ActionEvent actionEvent) {
    String resultText = "";
    Translator translator = _translatorPanel.getTranslator();
    Word word = _wordPanel.getWord();
    Language targetLanguage = _translatorPanel.getTargetLanguage();
    TranslateWord translateMethod = new TranslateWord(translator, word, targetLanguage);
    try {
      Word result = (Word) translateMethod.makeCall();
      resultText = result.toString();
    }
    catch (CouldNotTranslateException couldNotTranslateException) {
      resultText = COULD_NOT_TRANSLATE_STRING;
    }
    catch (Exception e) {
      resultText = e.toString();
    }
    finally {
      _resultsPanel.setText(resultText);
    }
  }
}

This is actually pretty similar to the code we used in the very first implementation. The only difference is that instead of calling the translate method directly, we create an instance of TranslateWord and call its makeCall method.

Here's the source for TranslateWord:

import java.rmi.*;
import java.rmi.server.*;

public class TranslateWord extends AbstractRemoteMethodCall {

  private Translator _translator;
  private Word _sourceWord;
  private Language _targetLanguage;

  public TranslateWord(Translator translator, Word sourceWord, Language targetLanguage) {
    _translator = translator;
    _sourceWord = sourceWord;
    _targetLanguage = targetLanguage;
  }

  protected Remote getRemoteObject() throws ServerUnavailable {
    return (Remote) _translator;
  }

  protected Object performRemoteCall(Remote remoteObject) throws RemoteException, Exception {
    Translator translator = (Translator) remoteObject;
    return translator.translate(_sourceWord, _targetLanguage);
  }
}

Summary and Roadmap

In this article, we've discussed the basics of using command objects. We've built an abstract base class for our command objects, and then implemented the remote calls for the Translator application by extending the base class. If we were to perform a cost-benefit analysis now, we'd see something like the following:

Pro

Con

Opinions can vary. But I think that, even at this point, the pros significantly outweigh the cons.

Related Reading

Java RMIJava RMI
By William Grosso
Table of Contents
Index
Sample Chapter
Full Description

In the next article in this series, we'll discuss the third con, the fact that the framework is incomplete, in detail. We'll discuss why and how to build a local cache of stubs, and show how using command objects makes implementing a stub cache much cleaner. As part of this, we'll move all of the code that interacts with the RMI registry into the command objects (more precisely, we'll move the code that interacts with the RMI registry into an new abstract base class which extends AbstractRemoteMethodCall).

And in the final article of this series, we'll discuss the newly defined generics extension to Java and show how it partially addresses the last con by allowing us to return strongly typed values (instead of instances of Object).

William Grosso is a coauthor of Java Enterprise Best Practices.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.