Abstract: It can be a real pain to get RMI working, as there are loads of different things that can go wrong, both at the compilation and the deployment stages. This document describes how to negotiate this mess and end up with something that works.
Firstly, the arrangement of the code I shall be illustrating with is that there is a body of code which provides some service (the server), and another body of code that uses the service (the client). The server is to know nothing about the client, and the client is to only have extremely limited access to the server (which may even be running on a different machine on different filesystems.)
Secondly, make sure you use the same version of the Java environment throughout. There are some weird faults that can happen if you don't!.
The first thing you should do is to write your
remote interface(s), all of which must extend java.rmi.Remote
. Each of the methods in those
remote interfaces must throw java.rmi.RemoteException
at least. It is also
advisable to make sure that all the parameters to the methods and
results of the methods are primitive types (like int
and
void
), objects that implement or interfaces that extend
java.io.Serializable
, interfaces that extend java.rmi.Remote
(your objects should do one or
other of them, and your implementations must do this for
things to work at all) or arrays of these things (or arrays of arrays
of ...) It is also highly recommended that you make sure that all
classes that implement java.io.Serializable
(whether directly or
indirectly) contain a serialVersionUID
field; the
serialver program can help here.
Now, you write your objects that provide
implementations of all the remote interfaces. You will probably want
to make these extend java.rmi.server.UnicastRemoteObject
, though there
are other ways to do this as well. This done (and everything on the
server-side compiled up) you need to run the rmic program on
each remote implementation to build the stubs and skeletons that do
all the communications for you. This done, you should have the
server-side code built properly.
Before you can start building the client though,
you need to partition the server code into three parts; the public
interface, the remote stubs, and the private server implementation.
The public interface should be the remote interfaces themselves, plus
any classes they mention (and whatever classes those classes
mention too; you need to form a transitive closure here of all the
classes mentioned so that people can manipulate the arguments to the
remote methods) and whatever support classes you want to give clients
direct access to. The second partition (which overlaps with
the first) contains the remote interfaces and the remote stubs (which
are named the same as the remote implementation, but with
_Stub
appended to the class name) created by
rmic (but not the skeletons) plus all the classes those
remote interfaces and stubs mention, but nothing else (provided you've
included all your exceptions that can be thrown across a remote method
call in the first partition.) The third partition is all the rest of
the server code, including the remote object skeletons created by
rmic (if it created any at all, that is.)
Now that you've formed this partition, you should
build three JAR files (this is the easiest way), one with the classes
in the first partition (which I'll call public.jar
for
convenience through the rest of this document), one with the classes
in the second partition (which I'll call remote.jar
) and
one with the classes from all three partitions (which I'll call
server.jar
). The public.jar
should be
handed to people developing clients to put on their classpath when
compiling and running the application, and the remote.jar
should be placed in a well-known place such as on an FTP or
HTTP server. It is recommended that the server author or publisher
should use jarsigner to apply a digital signature to
remote.jar
, and signing public.jar
and
server.jar
is not a bad idea too, though you do not need
to use the same signatures for them.
The client author should use just the classes in
public.jar
when writing their code, and they don't need
to take any special action at this point (though it is a smart move to
build the resulting class files into a JAR file too, which I'll call
client.jar
.)
To get the server running, you will need to first
create a suitable security policy file (policytool can help
here, but it is perfectly feasable to do this by hand too.) All this
needs to do is grant server.jar
whatever level of
permissions are appropriate, though you may well find that
java.security.AllPermission
is the most appropriate. It
all depends on how much you trust the server code. Note that you can
use either the codeBase or the signer of the server code to specify
what code to trust; using the signer makes it much easier to move
things around...
Before running the server, make sure you have an
instance of rmiregistry running on the same machine where you
want to run the server (server objects can only be registered on the
same machine that they are actually hosted on) and make sure that your
CLASSPATH
variable is empty (or at least contains none of
the JARs created above) when you run this command. Then all you need
to do is start up java with the right options and you're away. The
right options (which must go before the name of the class to start the
execution at) are:
-Djava.security.manager
You only need this option if you don't install a security manager in the server implementation itself, but you must have a security manager to be able to get the RMI registry stub (part of the server object registration process.)
-Djava.security.policy=
thePolicyFileYouCreatedAboveYou need this because the default security level is definitely far too restrictive; you won't even be able to contact the RMI registry to publish the server object(s).
-Djava.rmi.server.codebase=
thePublishedUrlForRemote.jarIt is through this that clients are told where to locate the stubs. Get it right.
-classpath server.jar
Where to find the code to run; note that old
versions of java (pre-1.2) should replace this with a suitably set
CLASSPATH
environment variable. This alone is one good
reason for switching to Java 1.2 or later...
Now, so long as your server code remembers to call
java.rmi.Naming.bind
or java.rmi.Naming.rebind
to register the remote
object implementation(s), everything should be fine, though you should
read Tips and Tricks below for some helpful extras.
Client authors will also need to create a suitable
security policy; the key entry will be one allowing code in the
remote.jar
codeBase (or which has been signed by the
signer of that JAR) to open sockets (with port numbers of at least
1024) to the machine that hosts the server objects. An example policy
file entry might be:
grant codeBase "http://www.bar.com/spong/remote.jar" { permission java.net.SocketPermission "foo.bar.com:1024-", "connect,resolve"; };
Once you've set your policy file up (remember to
allow the client code to do what it needs to do too, such as reading
files and opening sockets; java.security.AllPermission
is
useful here too) all you need to do is to start up the client with
both public.jar
and client.jar
on the
class-path, a security manager installed (either on the command line
or in the client implementation) and the location of the policy file.
At this point, the client should be able to use java.rmi.Naming.lookup
to obtain a server stub and
then invoke methods on it. It Should All Just WorkTM
Many of these parameters that are passed to clients and servers when invoking them are the same each time; use a small shell script to set them up and avoid loads of hassle!
There are a number of fairly-poorly-documented
extra system properties that you can set. The ones whose names
start with java.
are part of the RMI specification, and
the ones starting with sun.
are part of Sun's
implementation.
java.rmi.server.logCalls
A boolean property (i.e. assign
true
to it to turn it on) that turns on logging of
what calls of objects are being made. Useful for figuring out what
method calls are actually being done.
sun.rmi.loader.logLevel
A logging property (i.e. assign
BRIEF
or VERBOSE
to it to turn it on)
that allows you to find out when classes are being loaded, which
can be useful in debugging class-loader problems.
sun.rmi.server.exceptionTrace
A boolean property that makes every remote method call that returns an exception first print out the stack trace from that exception. This is critical when debugging code that is being called remotely, because traces are not preserved between VMs.
sun.rmi.server.logLevel
A logging property that allows you to find
out what remote method calls are being made. Note that this tends
to produce a lot of output, even on BRIEF
...
If you are using the 1.3 JDK (or later), you
can use java.lang.Runtime.addShutdownHook
to help ensure
that your server unregisters itself correctly (using java.rmi.Naming.unbind
) from the RMI registry at
program termination. This includes running when you halt the program
with Control+C
, so it is a useful technique in
practise.
The most common problems are:
java.rmi.server.codebase
property on the
server. The RMI mechanism passes this on to clients
automatically.serialVersionUID
field.rmiregistry
. It must be
running, and it must be running on the same machine as the class
publishing the initial remote class instance (a.k.a. the server)
because the Sun RMI registry only lets classes register stubs if
they are connecting from the same machine.Note that many of the above problems can manifest
themselves in the oddest of ways, and some of them can produce the
same exception (the most inscrutable of which is java.lang.NoClassDefFoundError
since it doesn't
explain why there is a problem) but for different reasons.
This sucks, and often the debugging properties don't help all that
much either.
Any feedback on this? Email me and let me know so I can make this document better. Continued improvements will depend on your input...
Donal K. Fellows / Department of Computer Science, University of Manchester