The RemoteCallback Pattern
Intent
Use RMI for callbacks with a variety of different client classes,
without exporting them to the server, or trusting them on the server.
Motivation
Some systems use callbacks to transfer control information from client
to server, without wanting to execute code on the server. Client code
may change much more frequently than server code, and the server might
not wish to run any bytecodes provided by client-controlled web servers.
This pattern allows client code to receive callbacks from the server
via a fixed wrapper class that would be as stable as the interface
class. Client side code to be called back need not implement Remote,
have rmic run on it, or ever leave the client. Each client could use
different callback classes.
The server is protected from client provided bytecodes, because there
aren't any. The worst a misbehaving client can do is to tie up a thread
of control by hanging in the remote call, or throw an exception. It
cannot use any unexcepted amount of server side cpu or memory (per call).
This pattern is detailed enough to be almost completely automated
using code generation. A code generator for it is provided.
Applicability
Use the RemoteCallback pattern when
-
Callback routines are required only for transfer of control, rather
than to offload cpu consumption from client to server.
-
It is impractical, or dangerous, for the server to receive stub class
bytecodes from web servers.
Structure
This is a specialisation of Proxy to the circumstances of Java RMI.
Recall that objects implementing Remote are passed over RMI by
references (strictly speaking, by serializing Stub classes in their
place), whilst other objects are passed in serialized form. We split
the callback interface into two interfaces. One interface, Callback in
my example code, declares that
all methods throw RemoteException, but does not extend Remote. This
interface is the one implemented by client classes to be called back.
Since it does not implement Remote, there is no point using rmic on
these classes. If these classes were to pass over RMI, they would have
to go in serialized form, but in fact they need never be transferred
from server to client or vice versa.
The second interface, RemoteCallback in my example code, inherits from
Callback, and
also from Remote. This is why the methods in the first
interface need to declare RemoteException: this interface must, and it
must also inherit from the first interface. RemoteCallback
need be implemented by only one class,
which I will call CallbackWrapper. This contains a single
private member of the type of the
first interface. It works by delegating all interface methods to this
member.
CallbackWrapper can, and
probably should, be final. Any classes produced by subclassing it
would need their own stubs, and so would (and probably should) fail at
run time. We can prevent this at compile time by prohibiting
subclasses via final.
Since both RemoteCallback and CallbackWrapper are completely
predictable from the first interface, they are as stable as it is,
and could be automatically produced from it. Sun have pointed out that
it is quite easy to understand much of the structure of a .java file
be plugging a java class extending Doclet into javadoc.
Also included here is a
java doclet, CallbackGenDoclet.java, which does just this.
Client code registers a callback by constructing an object of any
class (say Client) implementing Callback, and then passing
it to the constructor of the CallbackWrapper class. This provides the
client
with an object of a class, CallbackWrapper, already known to both
server and client.
The client can pass this CallbackWrapper object to the server,
by making whatever RMI call the
server has defined. This involves transferring only a serialized
CallbackWrapper_stub, bytecodes for which could already be in the
server's CLASSPATH. When the server makes the
callback, only the stub code inside CallbackWrapper_Stub will be
executed on the server. This code will immediately make an RMI
call back to the client, where the CallbackWrapper code will delegate
to a method in the client class implementing Callback.
Consequences
This deliberately provides only a subset of the possibilities of RMI.
It is designed to stop the server dynamically loading code
from the client, which means that it cannot take cpu load off the
client, or provide flexible access to resources (e.g. IO or memory),
by hosting client code.
Implementation
Is described in the accompanying code. If the server really
does not trust the client code it will probably have to catch
exceptions thrown by client callback routines (including e.g.
NullPointerException, which will be helpfully serialized by
RMI when it occurs, transferred over to the server side, and thrown
there). It will probably also want to devote a separate thread to each
client, and use some form of timeout mechanism, to stop misbehaving
clients delaying service to others, or tying up server threads, by
hanging in the callback. This is not obvious from the example code,
because the example callback is immediate and happens in an RMI
service thread, which is probably not likely in real life.
Can this be done via some lesser-known RMI facility? I don't know, but I
wouldn't be surprised if it could be done by overriding some of the
methods in UnicastRemoteObject. In practice, I suspect that this is
done a lot by providing the server with stub classes generated from
client classes that are many versions behind the classes actually
running on the client, and observing that this appears to work.
Related Patterns
-
Proxy
-
There is a very vague resemblance to EJBs here: both use RMI calls to an
object that delegates its behaviour to another, but that's about all.
Example
RemoteCallback.zip is a jar file
containing demonstration Java code showing a client and server using
this pattern (in fact this code uses the pattern in both directions).
It also contains batch files build.bat and run.bat to build and run
this test, and a copy of this html file. The batch files
buildCodeGen.bat and runCodeGen.bat do the same thing, but using
auto-generation via javadoc. Unfortunately this means that you will have
to edit buildCodeGen.bat to fill in the location of tools.jar on your
machine.