O'Reilly logo

Java RMI by William Grosso

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

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.



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

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required