Chapter 4.  The Same Server, Written Using RMI

In this chapter, we continue our discussion by reimplementing the printer server using RMI as a distribution mechanism instead of sockets. As part of doing so, I will introduce the core infrastructure of RMI in a familiar setting. In fact, the application itself will look remarkably similar to the socket-based version. By the end of this chapter, you will have received a glimpse at how an RMI application is structured and the basic sequence of steps required to build one.

The Basic Structure of RMI

In the previous chapter, we covered the basics of implementing a socket-based distributed application. In doing so, we reinforced the lessons of the previous two chapters on streams and sockets. In addition, we discovered that the code necessary for writing a socket-based distributed application falls into five basic categories:

  • Code that actually does something useful. This code is commonly referred to as business logic.[20] An example is an implementation of the Printer interface.

  • User interface code for the client application.

  • Code that deals with marshalling and demarshalling of data and the mechanics of invoking a method in another process. This is tedious code, but it is straightforward to write and is usually a significant percentage of the application.

  • Code that launches and configures the application. We used a number of hard-wired constants (in NetworkBaseClass) to enable the client to initially connect with the server. And we wrote two main( ) methods—one to launch the client and one to launch the server.

  • Code whose sole purpose is to make a distributed application more robust and scalable. This usually involves one or more of the following: client-side caching (so the server does less work); increasing the number of available servers in a way that’s as transparent as possible to the client; using naming services and load balancing; making it possible for a server to handle multiple requests simultaneously (threading); or automatically starting and shutting down servers, which allows the server lifecycle to conserve resources.

In any distributed application, programmers need to write the first and second types of code; if they did not need to write the business logic, the application wouldn’t be necessary. Similarly, the user interface, which enables users to access the business logic, needs to be written for any application. And the fifth type, code that enables the application to scale, can be the most difficult and application-specific code to write.

The third and fourth types of code, however, are different. Most of this code can be automatically generated without much programmer thought or effort.[21] It may seem difficult to write marshalling code if you’ve never done so before. However, by the second time, it’s easy. By the third time, most programmers are flat-out bored by the task.

We will see in this chapter that RMI either already contains—or will automatically generate—most of the code in the third and fourth categories. Indeed, this alone is a compelling reason to use RMI.[22]

Methods Across the Wire

Though convenient, automatically generating marshalling and demarshalling code is mostly a side effect produced in the service of a much more important goal. In a nutshell:

RMI is designed to make communication between two Java programs, running in separate JVMs, as much like making a method call inside a single process as possible.

This is an ambitious goal. How does RMI achieve it?

Recall that in order to communicate with the printer server, we wrote an object, ClientNetworkWrapper, which did three things:

  • It opened a socket.

  • It told an instance of DocumentDescription to write itself to the stream.

  • It read and interpreted information from the input stream associated with the socket.

In addition, we wrote a companion object, ServerNetworkWrapper, which played an analogous role on the server side.

RMI relies on two similar types of objects that are automatically generated by the RMI Compiler from an implementation of the server: stubs and skeletons. A stub is a client-side object that represents a single server object inside the client’s JVM. It implements the same methods as the server object, maintains a socket connection to the server object’s JVM automatically and is responsible for marshalling and demarshalling data on the client side. A skeleton is a server-side object responsible for maintaining network connections and marshalling and demarshalling data on the server side.

Tip

The word stub is actually used to mean two different things. Depending on context, it might refer to a stub class that is automatically generated from a server class object. Alternatively, it might refer to an instance of a particular stub class (that is, to a reference to a specific instance of the server class). Because stubs have such a well-defined role in a distributed architecture, the meaning is usually clear from context. Similarly, skeleton can either refer to the skeleton class or to an instance of the skeleton class.

The basic procedure a client uses to communicate with a server is as follows:

  1. The client obtains an instance of the stub class. The stub class is automatically pregenerated from the target server class and implements all the methods that the server class implements.

  2. The client calls a method on the stub. The method call is actually the same method call the client would make on the server object if both objects resided in the same JVM.

  3. Internally, the stub either creates a socket connection to the skeleton on the server or reuses a pre-existing connection. It marshalls all the information associated to the method call, including the name of the method and the arguments, and sends this information over the socket connection to the skeleton.

  4. The skeleton demarshalls the data and makes the method call on the actual server object. It gets a return value back from the actual server object, marshalls the return value, and sends it over the wire to the stub.

  5. The stub demarshalls the return value and returns it to the client code.

Stubs and skeletons are shown in Figure 4-1.

A basic RMI call with a stub and skeleton

Figure 4-1. A basic RMI call with a stub and skeleton

If this approach seems familiar, it’s because the stub and the skeleton are really automatically generated, object-oriented versions of the objects we created for our socket-based printer server.

Let’s take a close look at this. Here is part of the stub generated for our NullPrinter class:

public final class NullPrinter_Stub extends java.rmi.server.RemoteStub implements 
    com.ora.rmibook.chapter4..Printer, java.rmi.Remote {
   ...
    // methods from remote interfaces
    
    // implementation of printDocument(DocumentDescription)
	public boolean printDocument(com.ora.rmibook.chapter4.DocumentDescription $param
         DocumentDescription_1) throws om.ora.rmibook.chapter4.PrinterException,
         java.rmi.RemoteException {
		try  {
			...
			java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.
                  RemoteObject) this, operations, 0, interfaceHash);
			try  {
				java.io.ObjectOutput out = call.getOutputStream(  );
				out.writeObject($param_DocumentDescription_1);
			} 
			catch (java.io.IOException e)  {
				throw new java.rmi.MarshalException("error marshalling
                       arguments", e);
			}
			ref.invoke(call);
			boolean $result;
			try {
				'java.io.ObjectInput in = call.getInputStream(  );
				result = in.readBoolean(  );
			}
			 catch (java.io.IOException e) {
				throw new java.rmi.UnmarshalException("error unmarshalling
                       return", e);
			} 
			finally  {
				ref.done(call);
			}
			return $result;

While this may seem a bit more complex than the code we wrote for the socket-based printer server (and the fact that we’re showing only part of the code indicates that stubs are actually quite a bit more complicated than the ClientNetworkWrapper class might have led us to expect), the fact remains: the stub implements the Printer interface, and the implementation of each method in the Printer interface simply pushes data onto a stream, and then reads data from a stream.

Tip

Strictly speaking, skeletons aren’t really necessary. They can be replaced by a more generic framework that uses Java’s reflection API to invoke methods on the server side. We’ll cover this in more detail in Chapter 8. In this book, however, our code uses skeletons.

Passing by Value Versus Passing by Reference

In the first section of this chapter, we stated that RMI automatically generates most of the marshalling and demarshalling code required to build a distributed application. It’s easy to see how RMI could automatically do this for primitive argument types. After all, an int is simply four consecutive bytes. Automatically marshalling and demarshalling objects, on the other hand, is a more difficult task. And, in order to do so correctly, RMI requires us to distinguish between two main types of objects: those that implement the Remote marker interface and those that implement the Serializable marker interface.

Tip

A marker interface doesn’t define any methods; it simply provides information (available by reflection) about other code. In this case, RMI checks to see whether a given object implements either Remote or Serializable and behaves differently in either case.

Remote objects are servers. That is, they have a fixed location and run in a specific JVM on a particular computer somewhere in the network; they are the objects that receive remote method invocations. In RMI, remote objects are passed by reference. That way, if two instances of some remote object type exist, they are logically distinct. For example, in the current application, each Printer is a remote object, and any two instances of Printer are not equal.

Serializable objects, on the other hand, are objects whose location is not important to their notion of identity. That is, while they do have a location, the location is not particularly relevant to their state. Instead, serializable objects encapsulate data and are mobile—they can be passed from one JVM to another. Hence, serializable objects are very much like the primitive datatypes, such as float and int, which are also always passed by value.

Note that if an argument is a remote object (e.g., a server), the skeleton doesn’t send a serialized copy of the server. Instead, it creates a stub that serves as a reference to that object and sends a serialized copy of the stub over the wire. What about arguments that are neither serializable nor remote? Well, if it’s a primitive datatype, it is passed by value as well. But if it’s an object and is neither serializable nor remote, an exception is thrown.

The Architecture Diagram Revisited

While the printer application is simple enough so that the RMI-based application is similar to the socket-based application, RMI does add one more conceptual wrinkle. Recall that in the socket-based version, we used a set of constants to help the client find the server:

public abstract class NetworkBaseClass {
	public static final String DEFAULT_SERVER_NAME = "localhost";
 	public static final int DEFAULT_SERVER_PORT = 2100;
	public static final int DEFAULT_SERVER_BACKLOG = 10;
....
}

That’s a bad design strategy. If the server is moved to another computer, or if you want to use the same client to talk to multiple servers, you need to deploy a new version of the client application.

A much better strategy is to have a centralized naming service. A naming service is an application that runs on a central server and functions like a phone book. In order for a client to connect to a server, it must do two things:

  1. It must connect to the naming service and find out where the server is running.

  2. It must then connect to the server.

At first glance, a naming service appears to suffer from the same design flaw as NetworkBaseClass. Instead of hardwiring the location of the server into our client code, we’re hardwiring the location of the naming service. There are, however, a number of differences that combine to make this a more palatable solution. Among the most significant are:

  • Naming services are fairly simple applications that place limited demands on a computer. This means that the server running a naming service doesn’t need to be upgraded often.

  • Naming services are stable applications with simple APIs. They are not updated or revised often.

  • The application may actually have several servers. Rather than hardwiring all their locations into the client application, we need only one predetermined location.

The first two are especially important. Some common reasons for moving a server application to a new machine include scaling the application or providing for changes in application functionality. In the case of a naming service, however, the hardware will likely be sufficient to handle the load for quite a long period of time. Moreover, because the naming service is a simple and well-understood application that rarely changes, chances are that the implementation is a reliable piece of code. In other words, a computer running a naming service can often be set up and left alone.

In RMI, the default naming service that ships with Sun Microsystem’s version of the JDK is called the RMI registry.[23] Messages are sent to the registry via static methods that are defined in the java.rmi.Naming class. Including the RMI registry in our printer application architecture leads to the diagram in Figure 4-2.

Adding an RMI registry to the architecture diagram

Figure 4-2. Adding an RMI registry to the architecture diagram

While we’ve introduced only one new server into our application, we’ve added two new types of messages to those that are flowing across the network:

The registry must be told about the printer server.

This must happen before any other types of messages can be sent. Note that the printer server is a remote object, so what really gets passed to the registry is a stub (recall that stubs are serializable and can therefore be passed by value). This stub knows on which computer the printer server runs, and on which port the skeleton receives messages. Thus, the stub can relay method calls to the printer server’s skeleton.

The client must communicate with the registry to find out how to connect with the printer server.

The client must do this before the client can actually request that a document be printed. As a result of this, the client will obtain a copy of the stub that the server originally sent to the registry.

And, of course:

The client must send print requests to the printer server.

All of the communication in the socket-based version of the printer server is of this type.

Tip

In order to provide distributed garbage collection, RMI also sends other types of messages dealing with renewing leases. We will cover these messages (which are sent automatically and don’t require any work by application developers) in Chapter 16.

Implementing the Basic Objects

Now let’s start implementing the RMI-based printer server. As in the socket-based version, we have three basic objects: the Printer interface, the DocumentDescription object, and the PrinterException object. Conceptually, these objects are the same as their counterparts in Chapter 3. However, as might be expected, using RMI will force us to change a few details.

The Printer Interface

There are two basic changes to the Printer interface: it now extends the Remote interface, and every method is defined to throw RemoteException, a subclass of Exception defined in the package java.rmi. This class is shown in Example 4-1.

Example 4-1. Printer.java

public interface Printer extends PrinterConstants, Remote {
	public boolean printerAvailable(  ) throws RemoteException;
	public boolean printDocument(DocumentDescription document) throws RemoteException,
         PrinterException;
}

That Printer extends Remote shouldn’t be a surprise—the whole point of the application is to turn a local printer into a server that can receive calls from clients’ applications running on other computers.

The other change involves adding RemoteException to each method signature. RemoteException is an exception thrown by RMI to signal that something unforeseen has happened at the network level. That is, it’s a way for the RMI infrastructure to tell a client application that “something went wrong in the RMI infrastructure.” For example, if the server crashes while handling a client’s request, RMI will automatically throw a RemoteException on the client side.

Adding RemoteException to every method has one important consequence. Recall that rmic is used to automatically generate a stub class for each implementation of Printer. This stub implements the Printer interface and, therefore, every method implemented by the stub is declared to throw RemoteException. However, because RemoteException is a checked exception, any client-side code trying to invoke a method on the server must do so inside a try/catch block and explicitly catch RemoteException.

Making RemoteException a checked exception is one of the most controversial design decisions in the entire RMI framework. On one hand, it forces programmers writing client-side code to think about how to gracefully handle network failures. On the other hand, it is often the case that the catch block for a RemoteException doesn’t do anything interesting. Moroever, forcing programmers to catch RemoteException in their client code merely winds up making the code much harder to read and understand.

Tip

Notice that the printDocument( ) method is still defined as throwing PrinterException. If the implementation of Printer throws a PrinterException, the RMI skeleton will automatically marshall the PrinterException object and send it across the wire to the stub. The stub will demarshall the instance of PrinterException and throw it again. At this point, the exception will be caught by the catch( ) block. What’s happening here is simple: since RMI, via the stub and skeleton, controls the communication between the client and server, it can also automatically propagate exceptions across the network and rethrow them on the client side. Contrast this to the socket-based version, where Printer returned a status argument that the client was free to ignore.

Implementing a Printer

In order to implement a printer, we need to do two things: write and compile the actual server code and generate the stub and skeleton using rmic. The code for NullPrinter itself is almost identical to the code used in the socket-based version. The only difference is that the NullPrinter class extends java.rmi.server.UnicastRemoteObject., as shown in Example 4-2.

Example 4-2. NullPrinter.java

public class NullPrinter extends UnicastRemoteObject implements Printer {
	private PrintWriter _log;
	public NullPrinter(OutputStream log) throws RemoteException  {
		_log = new PrintWriter(log);
	}

UnicastRemoteObject is a convenient base class for RMI servers. It handles most of the tasks associated with creating a server. For example, the constructors of UnicastRemoteObject cause a server socket to be created automatically and start listening for client method invocations. Restating this slightly: when we instantiate NullPrinter, it immediately starts listening on a port for client requests. The RMI infrastucture handles all the details of opening a socket and listening for client requests.

After compiling the server, we need to generate stubs and skeletons. The stubs and skeleton classes are in the same package as the server class (in this case, com.ora.rmibook.chapter4.printers). In this case, we simply use:

rmic -keep -d d:\classes com.ora.rmibook.chapter4.printers.NullPrinter

Examining the skeleton

Just as we briefly examined the generated stub, it’s also worth looking for a moment at the skeleton that’s generated. The generated skeleton has one major method, named dispatch( ). dispatch( ) is the method that actually forwards requests to the server. Here’s a snippet of code from the dispatch( ) method of our skeleton:

 public void dispatch(java.rmi.Remote obj, java.rmi.server.RemoteCall call,
     int opnum, long hash) throws java.lang.Exception {
	...	// validation and error-checking code omitted
	com.ora.rmibook.chapter4.printers.NullPrinter server = 
		(com.ora.rmibook.chapter4.printers.NullPrinter) obj;
	switch (opnum)  {
		case 0: // printDocument(DocumentDescription) {
			com.ora.rmibook.chapter4.DocumentDescription
                  		$param_DocumentDescription_1;
			 try  {
				java.io.ObjectInput in = call.getInputStream(  );
				$param_DocumentDescription_1 =
					 (com.ora.rmibook.chapter4.DocumentDescription)
                             		in.readObject(  );
			} 
			catch (java.io.IOException e)  {
				throw new java.rmi.UnmarshalException(
                       			"error unmarshalling arguments", e);
			} 
			catch (java.lang.ClassNotFoundException e)  {
				throw new java.rmi.UnmarshalException(
                       			"error unmarshalling arguments", e);
			}
			finally  {
				call.releaseInputStream(  );
			}
			boolean $result = server.printDocument($param_DocumentDescription_1);
			try  {
				java.io.ObjectOutput out = call.getResultStream(true);
				out.writeBoolean($result);
			} 
			catch (java.io.IOException e)  {
				throw new java.rmi.MarshalException(
                       			"error marshalling return", e);
			}
			 break;


		}
	}
}

Let’s look at the arguments of this method first. The method takes an instance of Remote, a RemoteCall, an int, and a long. These arguments have the following meanings:

  • The instance of Remote is actually an instance of NullPrinter.

  • RemoteCall is an object that encapsulates a socket connection. The instance of RemoteCall being passed is a connection to a particular client.

  • The integer is mapped to a particular method on the server. That is, when rmic compiles the stub and the skeleton, it numbers all the methods. Afterwards, instead of passing the method name, it passes the associated integer. This saves bandwidth and also makes the skeleton more efficient by allowing it to perform method dispatch based on integer comparisons, rather than using string comparisons.

  • The long is an integrity check. Each method defined in NullPrinter has a unique long associated with it. This long is a hash of the method name and all the arguments. Sending this hash, along with the method number, helps to prevent versioning problems.

So what does this method do? It essentially contains marshalling and demarshalling code, similar to the code written by hand for the socket-based version of the printer server.

The Data Objects

We still have two further objects to implement: DocumentDescription and PrinterException. Let’s start with PrinterException. Example 4-3 shows the source code for PrinterException.

Example 4-3. PrinterException.java

public class PrinterException extends Exception {
	private int _numberOfPagesPrinted;
	private String _humanReadableErrorDescription;

	public PrinterException(  ) {
 		// zero arg constructor needed for serialization
 	}

	public PrinterException(int numberOfPagesPrinted, String
         humanReadableErrorDescription) {
		_numberOfPagesPrinted = numberOfPagesPrinted;
		_humanReadableErrorDescription = humanReadableErrorDescription;
	}

	 public int getNumberOfPagesPrinted(  ) {
		return _numberOfPagesPrinted;
	}

	 public String getHumanReadableErrorDescription(  ) {
		return _humanReadableErrorDescription;


	}
}

This is exactly what a generic exception[24] should be; it has enough state for the catch block to print out or display a meaningful error message. You can easily imagine a client popping up a dialog box to tell the user what went wrong, as in the following code snippet:

catch (PrinterException printerException) {
	String errorMessage =  "Print failed after " + printerException
         getNumberOfPagesPrinted(  ) + " pages.";
	JOptionPane.showMessageDialog(SimpleClientFrame.this, errorMessage, 
         "Error in printing" , JOptionPane.INFORMATION_MESSAGE);
	_messageBox.setText("Exception attempting to print " + (_fileChooser 
         getSelectedFile(  )).getAbsolutePath(  ) + "\n\t Error was: " + 
         printerException getHumanReadableErrorDescription(  ));
}

Even more impressively, PrinterException has no “extra” networking code. For example, it does not contain any code that either reads or writes from a stream. This is possible because RMI automatically uses serialization to send objects over the wire.

DocumentDescription

The other object we pass by value is an instance of DocumentDescription. However, we have a problem here: DocumentDescription stores the document as an instance of InputStream, and InputStream doesn’t implement the Serializable interface. This means that the generic serialization mechanism won’t work with DocumentDescription. We’re going to have to write custom marshalling and demarshalling code ourselves. The code is shown in Example 4-4.

Example 4-4. DocumentDescription.java

public class DocumentDescription implements Serializable, PrinterConstants {
	private transient InputStream _actualDocument;
	private int _length;
	private int _documentType;
	private boolean _printTwoSided;
	private int _printQuality;

	public DocumentDescription(  ) {
	// zero arg constructor needed for serialization
	}
....
	private void writeObject(java.io.ObjectOutputStream out) throws IOException {
		out.defaultWriteObject(  );
		copy(_actualDocument, out);
	}

	private void readObject(java.io.ObjectInputStream in) throws IOException,
         ClassNotFoundException {
		in.defaultReadObject(  );
		ByteArrayOutputStream temporaryBuffer = new ByteArrayOutputStream(  );
		copy(in, temporaryBuffer, _length);
		_actualDocument = new DataInputStream(new ByteArrayInputStream(temporaryBuffer
              toByteArray(  )));
	}

We start by declaring _actualDocument to be transient. transient is a Java keyword that tells the serialization mechanism not to serialize the variable’s value out. We then implement writeObject( ), which does two things:

  • Calls out.defaultWriteObject( ). This invokes the generic serialization mechanism (which is the default) to write out all nontransient objects. That is, when out.defaultWriteObject( ) is called, everything but _actualDocument has been encoded in the stream.

  • Copies _actualDocument to the stream, exactly as we did for the socket-based version of the program.

Similarly, in readObject( ), we first call defaultReadObject( ), which retrieves all the nontransient values, including _length, from the stream. We then read _actualDocument from the stream.

Tip

Why doesn’t InputStream implement the Serializable interface? The answer is that InputStream is an abstract base class whose concrete subclasses often have machine-specific state. For example, File-InputStream explicitly refers to a file on a hard drive and probably has a file descriptor as part of its state. Making objects such as FileInputStream serializable makes very little sense, since you can’t guarantee that either the file or the file descriptor will be available (or meaningful) when the information is deserialized. Similarly, classes such as Frame or Thread, which encapsulate operating-system resources, are not serializable.

The Rest of the Server

To finish building our server, we need to write launch code. Launch code is code that is application-specific, but not business-domain specific, and handles the details of registering a server with a naming service such as the RMI registry. In our case, this boils down to two pieces of code: a Java program that runs PrinterServer and a batch file that starts the RMI registry and then runs our program. The former is shown in Example 4-5.

Example 4-5. SimpleServer.java

public class SimpleServer implements NetworkConstants {
	public static void main(String args[]) {
		try {
			File logfile = new File("C:\\temp\\serverLogfile");
			OutputStream outputStream = new FileOutputStream(logfile);
			Printer printer = new NullPrinter(outputStream);
			Naming.rebind(DEFAULT_PRINTER_NAME, printer);
		}
		catch (Exception e) {
			e.printStackTrace(  );
		}
	}
}

This creates an instance of NullPrinter and then binds it into the registry under the name DEFAULT_PRINTER_NAME. The only surprising detail is this: if everything is successful, our program will exit main( ). Don’t worry; this is normal. The fact that the RMI registry has a reference (e.g., a stub) for the server keeps the application alive even though we’ve exited. I’ll explain why, and how this works, in Chapter 16.

Note

Note that we used rebind( ) instead of bind( ) in our launch code. The reason is that bind( ) fails if the name we’re binding the server to is already in use. rebind( ), on the other hand, is guaranteed to succeed. If another server is bound into the registry using the name we want to use, that server will be unbound from the name. In reality, bind( ) is rarely used in launch code, but is often used in code that attempts to repair or update a registry.

The format of names that are bound into the registry is fairly simple: they follow the pattern //host-name:port-number/human-readable-name. host-name and port-number are used to find the registry.

The batch file, rmiprinterserver.bat, consists of the following two commands:

start rmiregistry
start java com.ora.rmibook.chapter4.rmiprinter.applications.SimpleServer

start is a Windows command that executes the rest of the line in a separate process. It is equivalent to putting an ampersand (&) after a command in a Unix shell. Thus, invoking rmiprinterserver.bat from the DOS shell launches the RMI registry in another process, launches SimpleServer in a third process, and then returns to the command prompt to wait for further instructions.

The Client Application

Once the changes to the data objects have been made and the skeletons and stubs have been generated from the server, the networking part of the client application is a remarkably straightforward piece of code. Recall that our client application had the GUI shown in Figure 4-3.

Printer/client application GUI

Figure 4-3. Printer/client application GUI

The only part of this that’s changed is the ActionListener attached to the Print File button. And it’s much simpler:

private class PrintFile implements ActionListener {
	public void actionPerformed(ActionEvent event) {
		try {
			FileInputStream documentStream = new FileInputStream(_fileChooser
                  getSelectedFile(  ));
			DocumentDescription documentDescription = new
			DocumentDescription(documentStream);

/*
			New network code follows
*/
			Printer printer = (Printer) Naming.lookup(DEFAULT_PRINTER_NAME);
			printer.printDocument(documentDescription);
		}
		catch (PrinterException printerException){
		....
		}
	}
	...
}

All this does is use a predetermined name, which must be the same name as the server used to bind, to locate an object inside the RMI registry. It then casts the object to the correct type (the RMI registry interface, like many Java interfaces, returns instances of Object) and invokes the printDocument( ) method on the server. And that’s it! We’ve finished reimplementing the socket-based printer server as an RMI application.

Tip

In this code example, as in many of the examples in this book, the client and server must be located on the same machine. This is because the call to Naming.lookup( ) simply used DEFAULT_PRINTER_NAME (with no hostname or port number specified). By changing the arguments used in the call to Naming.lookup( ), you can turn the example into a truly distributed application.

Summary

In this chapter, we’ve gone over the basics of developing an RMI application in a cookbook-style way, in order to get acquainted with the basic structure and components of an RMI application. Consequently, we glossed over many of the details. However, the key points to remember are:

  • Simple RMI applications are, in fact, not much more complicated than single-process applications.

  • RMI includes reasonable default solutions for the common problems in building distributed applications (serialization handles marshalling and demarshalling, the registry helps clients find servers, and so on).

  • Even when problems arise (e.g., DocumentDescription), the code is remarkably similar to, and simpler than, the analogous socket code.

  • The conceptual cost to using RMI isn’t all that high. In most cases, using RMI amounts to adding an extra layer of indirection to your code.

  • The application evolution problems mentioned in Chapter 3 aren’t nearly so forbidding when using RMI. The default mechanisms, and the automatically generated stubs and skeletons, handle many application evolution problems nicely.



[20] “Business logic” is actually a generic term that refers to the code that justifies the application’s existence (e.g., the code that actually implements the desired functionality).

[21] As a corollary, it ought to be generated automatically. Code that bores the programmer is code that is likely to contain errors.

[22] Or a similar object-distribution framework such as CORBA.

[23] More often referred to as simply “the registry.”

[24] This is a generic exception because it covers a wide range of devices. Since it’s impossible to define all the different types of exceptions a printer can generate (and create subclasses of PrinterException for each one), we simply rely on the user to interpret the exception. java.sql.SQLException follows a similar design strategy.

Get Java RMI now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.