ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Learning Command Objects and RMI
Pages: 1, 2, 3, 4

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.

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:

  • 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 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!

Pages: 1, 2, 3, 4

Next Pagearrow