The most fundamental means of inter-object communication in Java is method invocation. Mechanisms like the Java event model are built on simple method invocations between objects in the same virtual machine. Therefore, when we want to communicate between virtual machines on different hosts, it’s natural to want a mechanism with similar capabilities and semantics. Java’s Remote Method Invocation mechanism does just that. It lets us get a reference to an object on a remote host and use it as if it were in our own virtual machine. RMI lets us invoke methods on remote objects, passing real Java objects as arguments and getting real Java objects as returned values.
Remote invocation is nothing new. For many years C programmers have used remote procedure calls (RPC) to execute a C function on a remote host and return the results. The primary difference between RPC and RMI is that RPC, being an offshoot of the C language, is primarily concerned with data structures. It’s relatively easy to pack up data and ship it around, but for Java, that’s not enough. In Java we don’t just work with data structures; we work with objects, which contain both data and methods for operating on the data. Not only do we have to be able to ship the state of an object (the data) over the wire, but also the recipient has to be able to interact with the object (use its methods) after receiving it.
It should be no surprise that RMI uses object serialization, which allows us to send graphs of objects (objects and all of the connected objects that they reference). When necessary, RMI can also use dynamic class loading and the security manager to transport Java classes safely. Thus, the real breakthrough of RMI is that it’s possible to ship both data and behavior (code) around the Net.
Before an object can be used with RMI, it must be serializable. But that’s not sufficient. Remote objects in RMI are real distributed objects. As the name suggests, a remote object can be an object on a different machine; it can also be an object on the local host. The term remote means that the object is used through a special kind of object reference that can be passed over the network. Like normal Java objects, remote objects are passed by reference. Regardless of where the reference is used, the method invocation occurs at the original object, which still lives on its original host. If a remote host returns a reference to one of its objects to you, you can call the object’s methods; the actual method invocations will happen on the remote host, where the object resides.
Nonremote objects are simpler. They are just normal serializable objects. (You can pass these over the network as we did in Section 11.3.1 earlier.) The catch is that when you pass a nonremote object over the network it is simply copied. So references to the object on one host are not the same as those on the remote host. Nonremote objects are passed by copy (as opposed to by reference). This may be acceptable for many kinds of data-oriented objects in your application, especially those that are not being modified.
No, we’re not talking about a gruesome horror movie. Stubs and skeletons are used in the implementation of remote objects. When you invoke a method on a remote object (which could be on a different host), you are actually calling some local code that serves as a proxy for that object. This is the stub. (It is called a stub because it is something like a truncated placeholder for the object.) The skeleton is another proxy that lives with the real object on its original host. It receives remote method invocations from the stub and passes them to the object.
After you create stubs and skeletons you never have to work with them
directly; they are hidden from you (in the closet, so to speak).
Stubs and skeletons for your remote objects are created by running
the rmic
(RMI compiler) utility. After
compiling your Java source files normally, you run
rmic
on the remote object classes as a second
pass. It’s easy; we’ll show you how in the following
examples.
Remote objects are objects that
implement a special remote interface that
specifies which of the object’s methods can be invoked
remotely. The remote interface must extend the
java.rmi.Remote
interface. Your remote object will
implement its remote interface; as will the stub object that is
automatically generated for it. In the rest of your code, you should
then refer to the remote object as an instance of the remote
interface—not as an instance of its actual class. Because both
the real object and stub implement the remote interface, they are
equivalent as far as we are concerned (for method invocation);
locally, we never have to worry about whether we have a reference to
a stub or to an actual object. This “type equivalence”
means that we can use normal language features, like casting with
remote objects. Of course public fields (variables) of the remote
object are not accessible through an interface, so you must make
accessor methods if you want to manipulate the remote object’s
fields.
All methods in the remote interface must declare that they can throw
the exception
java.rmi.RemoteException
.
This exception (actually, one of many subclasses to
RemoteException
) is thrown when any kind of
networking error happens: for example, the server could crash, the
network could fail, or you could be requesting an object that for
some reason isn’t available.
Here’s a simple example of the remote interface that defines
the behavior of RemoteObject
; we’ll give it
two methods that can be invoked remotely, both of which return some
kind of Widget
object:
import java.rmi.*; public interface RemoteObject extends Remote { public Widget doSomething( ) throws RemoteException; public Widget doSomethingElse( ) throws RemoteException; }
The actual implementation of a remote object
(not the interface we discussed previously) will usually extend
java.rmi.server.UnicastRemoteObject
. This is the
RMI equivalent to the familiar Object
class. When
a subclass of UnicastRemoteObject
is constructed,
the RMI runtime system automatically “exports” it to
start listening for network connections from remote interfaces
(stubs) for the object. Like java.lang.Object
,
this superclass also provides implementations of equals( )
, hashcode( )
, and
toString( )
that make sense for a remote object.
Here’s a remote object class that implements the
RemoteObject
interface; we haven’t supplied
implementations for the two methods or the constructor:
public class MyRemoteObject implements RemoteObject extends java.rmi.UnicastRemoteObject { public RemoteObjectImpl( ) throws RemoteException {...} public Widget doSomething( ) throws RemoteException {...} public Widget doSomethingElse( ) throws RemoteException {...} // other non-public methods ... }
This class can have as many additional
methods as it needs; presumably, most
of them will be private
, but that isn’t
strictly necessary. We have to supply a constructor explicitly, even
if the constructor does nothing, because the constructor (like any
method) can throw a RemoteException
; we therefore
can’t use the default constructor.
What if we can’t or don’t want to make our remote object
implementation a subclass of UnicastRemoteObject
?
Suppose, for example, that it has to be a subclass of
BankAccount
or some other special base type for
our system. Well, we can simply export the object ourselves using the
static method exportObject( )
of
UnicastRemoteObject
. The exportObject( )
method takes as an argument a Remote
interface and accomplishes what the
UnicastRemoteObject
constructor normally does for
us. It returns as a value the remote object’s stub. However,
you will normally not do anything with this directly. In the next
section, we’ll discuss how to get stubs to your client through
the RMI registry.
Normally, exported objects listen on individual
ephemeral (randomly assigned) port numbers by default. (This is
implementation-dependent.) You can control the port number allocation
explicitly by exporting your objects using another form of
UnicastRemoteObject.exportObject( )
, which takes
both a Remote
interface and a port number as
arguments.
Finally, the name UnicastRemoteObject
suggests the
question, “what other kinds of remote objects are there?”
Right now, none. It’s possible that Sun will develop remote
objects using other protocols or multicast techniques in the future.
They would take their place alongside
UnicastRemoteObject
.
The registry is the RMI phone book. You use the registry to look up a reference to a registered remote object on another host. We’ve already described how remote references can be passed back and forth by remote method calls. But the registry is needed to bootstrap the process: the client needs some way of looking up some initial object.
The registry is implemented by a class
called Naming
and an application called
rmiregistry
. This application
must be running on the local host before you start a Java
program that uses the registry. You can then create instances of
remote objects and bind them to particular names in the registry.
(Remote objects that bind themselves to the registry sometimes
provide a main( )
method for
this purpose.) A registry name can be anything you choose; it takes
the form of a slash-separated path. When a client
object wants to find your object, it constructs a special URL with
the rmi:
protocol, the hostname, and the object
name. On the client, the RMI Naming
class then
talks to the registry and returns the remote object reference.
Which objects need to register themselves with the registry? Well, initially any object that the client has no other way of finding. But a call to a remote method can return another remote object without using the registry. Likewise, a call to a remote method can have another remote object as its argument, without requiring the registry. So you could design your system such that only one object registers itself, and then serves as a factory for any other remote objects you need. In other words, it wouldn’t be hard to build a simple object request “bouncer” (we won’t say “broker”) that returns references to all of the remote objects that your application uses. Depending on how you structure your application, this may happen naturally anyway.
Why avoid using the registry for everything? The current RMI registry is not very sophisticated, and lookups tend to be slow. It is not intended to be a general-purpose directory service (like JNDI, the Java API for accessing directory/name services), but simply to bootstrap RMI communications. It wouldn’t be surprising if Sun releases a much improved registry in the future, but that’s not the one we have now. Besides, the factory design pattern is extremely flexible and useful.
The first thing
we’ll
implement using RMI is a duplication of the simple
serialized object protocol from the
previous section. We’ll make a remote RMI object
called MyServer
on which we can invoke methods to
get a Date
object or execute a
WorkRequest
object. First, we’ll define our
Remote
interface:
//file: RmtServer.java import java.rmi.*; import java.util.*; public interface RmtServer extends Remote { Date getDate( ) throws RemoteException; Object execute( WorkRequest work ) throws RemoteException; }
The RmtServer
interface extends the
java.rmi.Remote
interface, which identifies
objects that implement it as remote objects. We supply two methods
that take the place of our old protocol: getDate( )
and execute( )
.
Next, we’ll implement this interface in a class called
MyServer
that defines the bodies of these methods.
(Note that a more common convention for
naming the implementation of remote
interfaces is to postfix the class name with
"Impl
“. Using that convention
MyServer
would instead be named something like
ServerImpl
.)
//file: MyServer.java import java.rmi.*; import java.util.*; public class MyServer extends java.rmi.server.UnicastRemoteObject implements RmtServer { public MyServer( ) throws RemoteException { } // implement the RmtServer interface public Date getDate( ) throws RemoteException { return new Date( ); } public Object execute( WorkRequest work ) throws RemoteException { return work.execute( ); } public static void main(String args[]) { try { RmtServer server = new MyServer( ); Naming.rebind("NiftyServer", server); } catch (java.io.IOException e) { // problem registering server } } }
MyServer
extends
java.rmi.UnicastRemoteObject
, so when we create an
instance of MyServer
, it will automatically be
exported and start listening to the network. We start by providing a
constructor, which must throw RemoteException
,
accommodating errors that might occur in exporting an instance. (We
can’t use the automatically generated default constructor,
because it won’t throw the exception.) Next,
MyServer
implements the methods of the remote
RmtServer
interface. These methods are
straightforward.
The last method in this class is main( )
. This
method lets the object set itself up as a server. main( )
creates an instance of the MyServer
object and then calls the static method Naming.rebind( )
to register the object with the registry.
The arguments to rebind( )
are the name of the
remote object in the registry (NiftyServer
), which
clients will use to look up the object, and a reference to the server
object itself. We could have called bind( )
instead, but rebind( )
is less prone to problems:
if there’s already a NiftyServer
registered,
rebind( )
replaces it.
We wouldn’t need the main( )
method or this
Naming
business if we weren’t expecting
clients to use the registry to find the server. That is, we could
omit main( )
and still use this object as a remote
object. We would be limited to passing the object in method
invocations or returning it from method invocations—but that
could be part of a factory design, as we discussed
before.
//file: MyClient.java import java.rmi.*; import java.util.*; public class MyClient { public static void main(String [] args) throws RemoteException { new MyClient( args[0] ); } public MyClient(String host) { try { RmtServer server = (RmtServer) Naming.lookup("rmi://"+host+"/NiftyServer"); System.out.println( server.getDate( ) ); System.out.println( server.execute( new MyCalculation(2) ) ); } catch (java.io.IOException e) { // I/O Error or bad URL } catch (NotBoundException e) { // NiftyServer isn't registered } } }
When we run MyClient
, we pass it the hostname of
the server on which the registry is running. The main( )
method
creates an instance of the MyClient
object,
passing the hostname from the command line as an argument to the
constructor.
The constructor for MyClient
uses the hostname to
construct a URL for the object. The URL will look something like
this: rmi://hostname/NiftyServer. (Remember,
NiftyServer
is the name under which we registered
our RmtServer
.) We pass the URL to the static
Naming.lookup( )
method. If all goes well, we get back
a reference to a RmtServer
(the remote interface).
The registry has no idea what kind of object it will return;
lookup( )
therefore returns an
Object
, which we must cast to
RmtServer
.
Compile all of the code. Then run
rmic
, the RMI compiler, to make the stub
and skeleton files for MyServer
:
% rmic MyServer
Let’s run the code. For the first pass, we’ll assume that
you have all of the class files, including the stubs and skeletons
generated by rmic
, available in the class path on
both the client and server machines. (You can run this example on a
single host to test it if you want.) Make sure your class path is
correct and then start the registry; then start the server:
%rmiregistry &
(on Windows:start rmiregistry
) %java MyServer
In each case, make sure the registry application has the class path including your server classes so that it can load the stub class. (Be warned, we’re going to tell you to do the opposite later as part of setting up the dynamic class loading!)
Finally, on the client machine, run MyClient
,
passing the hostname of the server:
%java MyClient
myhost
The client should print the date and the number 4, which the server graciously calculated. Hooray! With just a few lines of code you have created a powerful client/server application.
Before running the example, we told you to distribute all the class files to both the client and server machines. However, RMI was designed to ship classes, in addition to data, around the network; you shouldn’t have to distribute all the classes in advance. Let’s go a step further, and have RMI load classes for us, as needed. This involves several steps.
First, we need to tell RMI where to find any other classes it needs.
We can use the system property
java.rmi.server.codebase
to specify a
URL on a web server (or FTP server) when
we run our client or server. This
URL specifies the location of a JAR file or a base directory in which
RMI will begin its search for classes. When RMI sends a serialized
object (i.e., an object’s data) to some client, it also sends
this URL. If the recipient needs the class file in addition to the
data, it fetches the file at the specified URL. In addition to stub
classes, other classes referenced by remote objects in the
application can be loaded dynamically. Therefore, we don’t have
to distribute many class files to the client; we can let the client
download them as necessary. In Figure 11.3, we
see an example as MyClient
is going to the
registry to get a reference to the RmtServer
object. Then MyClient
dynamically downloads the
stub class for
RmtMyServer
from a web server running on the
server object’s host.
We can now split our
class files between
the server and client machines. For example, we could withhold the
MyCalculation
class from the server, since it
really belongs to the client. Instead, we can make the
MyCalculation
class available via a web server on
some machine (probably our client’s) and specify the URL when
we run MyClient
:
java -Djava.rmi.server.codebase="http://myserver/foo/" ...
In this case, we would expect that MyCalculation
would be accessible at the URL http://myserver/foo/MyCalculation.class/.
(Note that the trailing slash in the URL is important: it says that
the location is a base directory that contains the class
files.)
Next we have to set up
security.
Since we will be loading class files over the network and executing
their methods, we must have a security manager in place to restrict
the kinds of things those classes may do, at least in the case where
they are not coming from a trusted code source. RMI will not load any
classes dynamically unless a security manager is installed. One easy
way to meet this condition is to install the
RMISecurityManager
as the system security manager for your
application. It is an example security manager that works with the
default system policy and imposes some basic restrictions on what
downloaded classes can do. To install the
RMISecurityManager
, simply add the following line
to the beginning of the main( )
method of both the
client and server applications (yes, we’ll be sending code both
ways in the next section):
main( ) { System.setSecurityManager( new RMISecurityManager( ) ); ...
The RMISecurityManager
will work with the system
security policy file to enforce restrictions. So you’ll have to
provide a policy file that allows the client and server to do basic
operations like make network connections. Unfortunately allowing all
of the operations needed to load classes dynamically would require us
listing a lot of permission information and we don’t want to
get into that here. So we’re going to resort to suggesting that
for this example you simply grant the code all permissions. Here is
an example policy file—call it
mysecurity.policy
:
grant { permission java.security.AllPermission ; };
(It’s exceedingly lame to install a security manager and then tell it to enforce no real security, but we’re more interested in looking at the networking code at the moment.)
So, to run our MyServer application we would now do something like this:
java -Djava.rmi.server.codebase='http://myserver/foo/' -Djava.security.policy=mysecurity.policy MyServer
Finally, there is one last magic incantation required to enable
dynamic class loading. As of the current implementation, the
rmiregistry
must be run without the classes which are
to be loaded being in its class path. If the classes are in the class
path of rmiregistry
, it will not annotate the
serialized objects with the URLs of their class files and no classes
will be dynamically loaded. This limitation is really annoying; all
we can say is to heed the warning for now.
If you meet these conditions, you should be able to get the client to
run starting with only the MyClient
class and the
RmtServer
remote interface. All of the other
classes will be loaded dynamically from a remote
location.
So far, we haven’t done
anything that we couldn’t have
done with the simple object protocol. We only used one remote object,
MyServer
, and we got its reference from the RMI
registry. Now we’ll extend our example to pass some remote
references between the client and server (these will be prime
candidates for dynamic class loading). We’ll add two methods to
our remote RmtServer
interface:
public interface RmtServer extends Remote { ... StringIterator getList( ) throws RemoteException; void asyncExecute( WorkRequest work, WorkListener listener ) throws RemoteException; }
getList( )
retrieves a new kind of object from the
server:
a
StringIterator
. The
StringIterator
is a simple list of strings, with
some methods for accessing the strings in order. We will make it a
remote object, so that implementations of
StringIterator
stay on the server.
Next we’ll spice up our work request feature by adding an
asyncExecute( )
method. asyncExecute( )
lets us hand off a
WorkRequest
object as before, but it does the
calulation on its own time. The return type for
asyncExecute( )
is void
,
because it doesn’t actually return a value; we get the result
later. Along with the request, our client passes a reference to a
WorkListener
object that is to be notified when
the WorkRequest
is done. We’ll have our
client implement WorkListener
itself.
Because this is to be a remote object, our interface must extend
Remote
, and its methods must throw
RemoteException
s:
//file: StringIterator.java import java.rmi.*; public interface StringIterator extends Remote { public boolean hasNext( ) throws RemoteException; public String next( ) throws RemoteException; }
Next, we provide a simple implementation of
StringIterator
, called
MyStringIterator
:
//file: MyStringIterator.java import java.rmi.*; public class MyStringIterator extends java.rmi.server.UnicastRemoteObject implements StringIterator { String [] list; int index = 0; public MyStringIterator( String [] list ) throws RemoteException { this.list = list; } public boolean hasNext( ) throws RemoteException { return index < list.length; } public String next( ) throws RemoteException { return list[index++]; } }
MyStringIterator
extends
UnicastRemoteObject
. Its methods are simple: it
can give you the next string in the list, and it can tell you whether
there are any strings that you haven’t seen yet.
Next, we’ll define the
WorkListener
remote interface. This is the
interface that defines how an object should listen for a completed
WorkRequest
. It has one method,
workCompleted( )
, which the server that is
executing a WorkRequest
calls when the job is
done:
//file: WorkListener.java import java.rmi.*; public interface WorkListener extends Remote { public void workCompleted(WorkRequest request, Object result ) throws RemoteException; }
Next, let’s add the new features to
MyServer
. We need to add implementations of the
getList( )
and
asyncExecute( )
methods, which we just added to
the RmtServer
interface:
public class MyServer extends java.rmi.server.UnicastRemoteObject implements RmtServer { ... public StringIterator getList( ) throws RemoteException { return new MyStringIterator( new String [] { "Foo", "Bar", "Gee" } ); } public void asyncExecute( WorkRequest request , WorkListener listener ) throws java.rmi.RemoteException { // should really do this in another thread Object result = request.execute( ); listener.workCompleted( request, result ); } }
getList( )
just returns a
StringIterator
with some stuff in it.
asyncExecute( )
calls a
WorkRequest
’s execute( )
method and notifies the listener when it’s done. (Our
implementation of asyncExecute( )
is a little
cheesy. If we were forming a more complex calculation we would want
to start a thread to do the calculation, and return immediately from
asyncExecute( )
, so the client won’t block.
The thread would call workCompleted( )
at a later
time, when the computation was done. In this simple example, it would
probably take longer to start the thread than to perform the
calculation.)
We have to modify
MyClient
to implement the remote
WorkListener
interface. This turns
MyClient
into a remote object, so we must make it
a UnicastRemoteObject
. We also add the
workCompleted( )
method that the
WorkListener
interface requires:
public class MyClient extends java.rmi.server.UnicastRemoteObject implements WorkListener { ... public void workCompleted( WorkRequest request, Object result) throws RemoteException { System.out.println("Async work result = " + result); } }
Finally, we want MyClient
to exercise the new
features. Add these lines after the calls to getDate( )
and execute( )
:
// MyClient constructor ... StringIterator se = server.getList( ); while ( se.hasNext( ) ) System.out.println( se.next( ) ); server.asyncExecute( new MyCalculation(100), this );
We use getList( )
to get the iterator from the
server, then loop, printing the strings. We also call
asyncExecute( )
to perform another calculation;
this time, we square the number 100. The second argument to
asyncExecute( )
is the
WorkListener
to notify when the data is ready; we
pass a reference to ourself (this
).
Now all we have to do is compile everything and run
rmic
to make the stubs for
all our remote objects:
rmic MyClient MyServer MyStringIterator
Restart the RMI registry and MyServer
on your
server, and run the client somewhere. You should get the following:
Fri Jul 11 23:57:19 PDT 1999 4 Foo Bar Gee Async work result = 10000
If you are experimenting with
dynamic class loading, you should be
able to have the client download all of the server’s auxiliary
classes (the stubs and the StringIterator
) from a
web server. And, conversely, you should be able to have the
MyServer
download the Client
stub and WorkRequest
related classes when it needs
them.
We hope that this introduction has given you a feel for the tremendous power that RMI offers through object serialization and dynamic class loading. Java is one of the first programming languages to offer this kind of powerful framework for distributed applications.
One of the newest features of RMI is the ability to create remote objects that are persistent. They can save their state and be reactivated when a request from a client arrives. This is an important feature for large systems with remote objects that must remain accessible across long periods of time. RMI activation effectively allows a remote object to be stored away—in a database, for example—and automatically be reincarnated when it is needed. RMI activation is not particularly easy to use and would not have benefited us in any of our simple examples; we won’t delve into it here. Much of the functionality of activatable objects can be achieved by using factories of shorter-lived objects that know how to retrieve some state from a database (or other location). The primary users of RMI activation may be systems like Enterprise JavaBeans, which need a generalized mechanism to save remotely accessible objects and revive them at later times.
Java supports an important alternative to RMI, called CORBA (Common Object Request Broker Architecture). We won’t say much about CORBA here, but you should know that it exists. CORBA is a distributed object standard developed by the Object Management Group (OMG), of which Sun Microsystems is one of the founding members. Its major advantage is that it works across languages: a Java program can use CORBA to talk to objects written in other languages, like C or C++. This is may be a considerable advantage if you want to build a Java front end for an older program that you can’t afford to re-implement. CORBA also provides other services similar to those in the Java Enterprise APIs. CORBA’s major disadvantages are that it’s complex, inelegant, and somewhat arcane.
Sun and OMG have been making efforts to bridge RMI and CORBA. There is an implementation of RMI that can use IIOP (the Internet Inter-Object Protocol) to allow some RMI-to-CORBA interoperability. However, CORBA currently does not have many of the semantics necessary to support true RMI style distributed objects. So this solution is somewhat limited at this time.