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.
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
|
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. |
|
|
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:
Possibly, there are multiple naming services (in our case, instances of the RMI registry) which have many different servers providing the same interface (if not the same level of functionality).
A typical client transaction involves querying one or more naming services, choosing from the set of available servers, and then making a remote method call (or calls) on the desired server.
The following diagram illustrates the application topology.

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:
Creates an RMI registry if necessary (using a static method defined on java.rmi.registry.LocateRegistry).
Creates two translators: an instance of EnglishToFrenchTranslator and an instance of EnglishToSpanishTranslator.
Binds these two servers into the registry it previously created.
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 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:

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.
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 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).

After all the data is entered, the user clicks 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:
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!
|
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:
20 implementations involving a retry loop and a time delay. Of course, there will be many slight implementation differences ("This remote call retries five times, with a wait of four seconds between retries" .... "This remote call retries three times, with waits of five, 10, and 15 seconds") with no overarching communication strategy and very little code reuse between the listeners.
Ten semi-correct implementations (with a retry loop but without delays, or where the retry logic isn't quite correct).
Seven places where the original 10-line try/catch/finally logic made it into the production code (and wasn't caught by QA because QA is on a nice network with a fast connection to the server).
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.
AbstractRemoteMethodCall and RetryStrategy classesThe 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.
RetryStrategy. This is basically an object wrapping an integer (the current number of tries). It tracks the number of attempts that have been made and implements the waiting strategy. In the code for these articles, I've made this into an abstract base class and implemented two concrete subclasses: AdditiveWaitRetryStrategy and ExponentialBackoffRetryStrategy.
AbstractRemoteMethodCall. This class encapsulates the retry logic in a single method named makeCall. In order to do this in a flexible way, it has a number of template methods, such as handleRetryException, which have default behaviors but which can be overridden by subclasses.
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).
|
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);
}
}
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:
Encapsulation is good.The encapsulating method calls in separate objects, which makes the main program logic easier to read. ClientFrame is easy-to-read code and all the details of making the remote call have been placed in a separate location.
Difficult logic is implemented once, in the framework. The retry logic is implemented once, in an abstract base class, and is correct.
The client code is simple. The code required to create a new subclass of AbstractRemoteMethodCall is almost trivial -- it's both easy to write and easy to see (at a glance) that it's correct.
The framework provides hooks. We have some very nice hooks for using different retry strategies based on context. We also have some very nice hooks for inserting logging functionality.
Indirection is confusing. Extra classes which encapsulate requests and the attendant level of indirection can be confusing. For small applications, using command objects feels like overkill.
Too much code. We haven't actually cut down the amount of code we need to write from the long 26-line while loop. The new code, while simpler, is just as lengthy.
It's an incomplete framework. Passing the stubs into the command objects feels strange -- surely the lookup code belongs inside the command object and not inside the object that simply wants to make the remote call.
We lost some type safety. The signature of makeCall -- returning instances of Object and throwing Exception -- is a violation of good programming practices. We've lost a fair amount of type safety here.
Opinions can vary. But I think that, even at this point, the pros significantly outweigh the cons.
|
Related Reading
|
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.