Jini is middleware for distributed processing. As such, it relies on a number of mechanisms for distributed processing. One of these is remote method invocation (RMI). In RMI, an object stays on a server and a dumb proxy is sent to a client. The client makes method calls on this proxy. The proxy just transmits calls across the network to the server, which calls the original object. The result of this method call is then sent back to the proxy which returns the result to the client. This is an object-oriented version of remote procedure calls. It is somewhat similar to CORBA remore references, except that the proxy is a Java object which runs in the client and does not require the "backplane" support of CORBA.
The original implementation of RMI used JRMP (Java Remote Method Procotol). This is a particular protocol built directly on TCP. Since then a number of other ways of doing RMI have emerged, such as RMI over HTTP (the Web transport protocol), RMI over IIOP (the CORBA transport protocol) and RMI over SSL (secure socket layer). There is even an implementation of RMI on Firewire (IEEE 1394), the high-speed transport layer designed for audio-visual data such as high-definition TV.
The different ways of doing RMI each have their own programmatic interface, with specialised classes. This should be abstracted: one mechanism, with configurations used to select the actual protocol implementation used. For example, a configuration file could specify whether to use RMI over TCP or RMI over IIOP. Then the application could be written in an implementation independant way, with protocol chosen at runtime based on runtime configuration information. This is what Jini has now done, and has developed a new protocol Jini ERI which solves some other issues.
Most books on Java include a section on RMI, and there are complete books just about this topic.
A class typically subclasses UnicastRemoteObject
. After compilation,
the RMI compiler rmic
is run on the class file to produce a proxy object.
When the service is run, this proxy object must be made network
visible in some way so that external clients can locate it.
This visibility may be to an RMI Naming
service, to a Jini registry or to
other directory services. Then a client can use the directory to find the proxy object
and use this to make remote calls on the original service object.
There is a little bit of chicanery before an object is made network visible: a class registers itself with the Java runtime by an operation called exporting, and then methods that should use the proxy object instead use the original service object. The Java runtime looks out for these and substitutes the proxy object in its place. This means that the programmer does not deal explicitly with the proxy at all,and seemingly writes code that does not use proxies. The Java runtime takes care of substituting the proxy when necessary. However, this may be a little confusing, and certainly does not make it clear exactly what is going on: this kind of trick is not one that will be in most programmer's experience.
The most common way to use RMI is simply to declare an object that extends
UnicastRemoteObject
and implements the Remote
interface
package jeri;
import java.rmi.*;
import java.rmi.server.*;
public class RmiImplicitExportDemo extends UnicastRemoteObject implements Remote {
public static void main(String[] args) throws Exception {
// this exports the RMI stub to the Java runtime
// a thread is started to keep the stub alive
new RmiImplicitExportDemo();
System.out.println("Proxy is now exported");
// this application will then stay alive till killed by the user
}
// An empty constructor is needed for the runtime to construct
// the proxy stub
public RmiImplicitExportDemo() throws java.rmi.RemoteException {
}
}
This does lots of things in the constructor behind the scenes: uses the class name to
construct a proxy object using the zero args constructor, starts an extra thread to
keep things alive and registers the object as a remote object requiring special attention.
While the intention is to keep things simple, these activities can prove to be a little
unsettling.
An alternative approach makes some of these more explicit
package jeri;
import java.rmi.*;
import java.rmi.server.*;
public class RmiExplicitExportDemo implements Remote {
public static void main(String[] args) throws Exception {
Remote demo = new RmiExplicitExportDemo();
// this exports the RMI stub to the Java runtime
RemoteStub stub = UnicastRemoteObject.exportObject(demo);
System.out.println("Proxy is " + stub.toString());
// This application will stay alive until killed by the user,
// or it does a System.exit()
// or it unexports the proxy
// Note that the demo is "apparently" unexported, not the proxy
UnicastRemoteObject.unexportObject(demo, true);
}
}
Traditionally, this mechanism has only been used when the class has to inherit from some other class and
cannot also inherit from UnicastRemoteObject
Jeri makes exporting an explicit operation, using a static method of an
Exporter
class. However, because it can be exporting objects that
use a variety of protocols (JRMP, IIOP, etc), it uses a number of levels of
indirection to gain a single compile-time object customised in different ways
at runtime. Firstly, it gains its Exporter
from a Configuration
object, which can contain a variety of runtime configuration information.
The Configuration
object is obtained from a ConfigurationProvider
.
The ConfigurationProvider
can be set to return a custom Configuration
,
but defaults to a ConfigurationFile
which takes a configuration file as
parameter.
The ConfigurationFile
object has a constructor
ConfigurationFile(String[] args)
. The first element of this
list is a filename for a configuration file. The list is
passed to the ConfigurationFile
constructor from
the ConfigurationProvider.getInstance(args)
method. Once a configuration
object is found, configurable objects can be extracted from it by the
Configuration.getEntry(String component, String name, Class type)
method.
The parameters to this are
To make this more concrete, the contents of the configuration file for a
ConfigurationFile
may be
import net.jini.jrmp.*;
JeriExportDemo {
exporter = new JrmpExporter();
}
This specifies an Exporter
object constructed from a
component name JeriExportDemo
with name exporter
, of type
JrmpExporter
(which is a subclass of Exporter
).
The configuration used can be changed by modifying the contents of the file or
by using different files such as jeri/iiop.config
:
import net.jini.iiop.*;
JeriExportDemo {
exporter = new IiopExporter();
}
or by using a file with multiple entries such as jeri/many.config
and using different component names to select which one is used:
import net.jini.jrmp.*;
import net.jini.iiop.*;
JrmpExportDemo {
exporter = new JrmpExporter();
}
IiopExportDemo {
exporter = new IiopExporter();
}
Once an Exporter
has been found, an object can be exported to the
Java runtime by the Exporter.export()
method. This takes the implementation
object and returns a proxy that can be registered with a lookup service or any other
remote directory.
The program to export an object using this configuration mechanism is
package jeri;
import java.rmi.*;
import net.jini.config.*;
import net.jini.export.*;
public class JeriExportDemo implements Remote {
private static String CONFIG_FILE = "jeri/jeri.config";
public static void main(String[] args) throws Exception {
String[] configArgs = new String[] {CONFIG_FILE};
// get the configuration (by default a FileConfiguration)
Configuration config = ConfigurationProvider.getInstance(configArgs);
System.out.println("Configuration: " + config.toString());
// and use this to construct an exporter
Exporter exporter = (Exporter) config.getEntry( "JeriExportDemo",
"exporter",
Exporter.class);
// export an object of this class
Remote proxy = exporter.export(new JeriExportDemo());
System.out.println("Proxy is " + proxy.toString());
// now unexport it once finished
exporter.unexport(true);
}
}
The exported object, the proxy, is declared to be of the Remote
interface. In fact,
the specification says that the proxy will implement all of the remote interfaces of the
original Remote
object. Here there are none, but in general a remote object
will implement one or more Remote
interfaces, and so will the proxy.
This last point is worth expanding on. Suppose we have an interface
Iface
which does not extend Remote
.
The interface RemoteIface
extends both Iface
and Remote
. From there we could have implementations
RemoteIfaceImpl
and IfaceImpl
, and we could
generate proxies from each of these using Exporter
.
The class names of the proxies are automatically generated (and are obscure)
so we shall call these classes RemoteIfaceImplProxy
and IfaceImplProxy
respectively. The resulting class diagram
is (with the non-standard dotted arrow showing the generation of the proxies)
IfaceImpl
does not implement any remote interfaces,
then neither does IfaceImplProxy
(strictly, it
may implement some additional ones, bit probably not
ones we are interested in here). There are no inheritance lines leading to
IfaceImplProxy
But since RemoteIfaceImpl
does implement a remote interface, then so does its proxy.
The most notable consequence of this is that the IfaceImplProxy
cannot be cast to an Iface
whereas
IfaceRemoteImplProxy
can be:
Iface iface = (Iface) ifaceImplProxy // class cast error
Iface iface = (Iface) ifaceRemoteImplProxy // okay
Thus it will be important to include a remote interface somewhere in the
interface hierarchy for any implementation.
The JrmpExporter
acts as a layer over the existing RMI runtime system, and just funnels
calls through to it. It acts as an adapter between existing methods for exporting and
unexporting methods over JRMP.
A class implements the Remote
interface. This will act as the implementation object
of some service. In order to create a proxy, the rmic
compiler will need to be run
on the implementation class file. For example,
javac JeriExportDemo.java
rmic -v1.2 -d . JeriExportDemo
This will create a JeriExportDemo_Stub.class
proxy. When the
Exporter.export()
method is called with the implementation object
the method will return an instance of this proxy class. This proxy instance
can then be registered with lookup services and sent to remote clients.
Jeri supplies an exporter that creates objects that use the Jini Extensible Remote Invocation protocol (Jini ERI, or JERI). This addresses a number of issues not handled by other protocols
rmic
)
Using Jini ERI instead of JRMP is no more complicated than changing the configuration file:
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
JeriExportDemo {
exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new BasicILFactory());
}
In fact, it is simpler to use Jini ERI since there is no need to create the proxy file at compile/build
time - it can be left to the runtime execution to build the proxy.
If you found this chapter of value, the full book is available from APress or Amazon . There is a review of the book at Java Zone . The current edition of the book does not yet deal with Jini 2.0, but the next edition will.