Getting Started With JINI 2.x

Creating a JINI 2.x Service

Introduction

This page explains the stages in creating a JINI service. It covers, basic JINI requirements and then goes on to cover such issues as adding support for using your service with com.sun.jini.start and security (in the form of constraints etc.). It represents a work in progress as there's much more detail to all these steps than I've covered so far. If you have a correction or there's something specific you'd like more information on, please drop me a line at Dancres.org.

Note: There is a "lighter" tutorial available on my blog.

Creating a Basic JINI Service

A basic JINI service consists of:
  1. A back-end service implementation (typically a remote object implemented using RMI or JERI).
  2. A proxy (either a remote reference of a "smart proxy").

Whilst it is perfectly possible to publish an RMI/JERI remote reference into the JINI lookup services, it is often more useful to publish a "smart proxy" which can provide additional features such as results caching, failover etc. Smart proxies also provide more flexibility with respect to communication with the back-end service as they permit the use of custom protocols over sockets etc.

A JINI service is expected to comply with certain requirements:
  1. Implement the Join protocol.
  2. Persist attributes (net.core.jini.entry.Entry instances).
  3. Persist it's ServiceID (which it can either generate itself or obtain from a lookup service at registration time).
  4. Persist it's current set of Lookup Groups and Lookup Locators .

The Join protocol can largely be implemented using net.jini.lookup.JoinManager. Persistence is slightly more problematic but can be handled using simple serialization. A JINI service can optionally implement certain administration interfaces such as JoinAdmin (which allows dynamic re-configuration of lookup attributes) or DestroyAdmin (which allows for permanent de-comissioning of a service).

For an example of how to use JoinManager, implement JoinAdmin and persistence of lookup attributes see my JINIExporter.

For an example of a simple JINI service, see the Hello example in the JINI starter kit available at Jini.org.

Note: For transient Jini services (those that don't persist their state) there is no requirement to persist join state.

Configuration

Most JINI services have a number of configurable parameters which may need altering/specifying at runtime. In previous versions of JINI, the mechanism by which this was achieved was left to the programmer. Fortunately, JINI 2.0 provides support for configuration files.

Examples of how to use configuration files can be found in Use net.jini.config.

One of the most important things that the configuration package provides is a mechanism by which the remote layers may be configured at deployment time via the specification of an exporter.....

Exporters

Exporters were introduced as part of JERI and are responsible for generating a remote reference for a remote object. This used to be done automatically via the practice of extending either UnicastRemoteObject or Activatable and invoking the parent constructor at runtime. However, you are strongly encouraged to avoid doing this as it makes porting your RMI servers over to the JERI framework more difficult. See Stop Relying on Automatic Stub Replacement.

Various exporters are provided to support a variety of remote communications protocols including:

  1. JRMP which will require the use of RMIC to generate stubs and skeletons.
  2. JERI which generates a dynamic proxy for use as a remote reference saving the need for using RMIC.
  3. IIOP which makes the server available via RMI-IIOP.
  4. Activation which wraps another exporter to generate a remote reference which is then made available via activation.
Various of these exporters accept Endpoint instances as parameters to their constructors which provides the means for the configuring a number of different transports including:
  1. TCP
  2. HTTP
  3. HTTPS
  4. SSL
  5. Kerberos

Because of the flexibility provided by the use of exporters, most services allow their exporter to be specified in a configuration. Such configuration file entries often look similar to this:

 import net.jini.jeri.tcp.TcpServerEndpoint;
import net.jini.jeri.ProxyTrustILFactory;
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;

serverExporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new ProxyTrustILFactory(null, null), false, true);

The service can then use the net.jini.config package to load the exporter and use it to generate a remote reference using something like:
 Exporter myDefaultExporter =
new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new BasicILFactory(), false, true);

if (theActivationID == null) {
theExporter =
(Exporter) myConfig.getEntry(MODULE_NAME,
"serverExporter",
Exporter.class,
myDefaultExporter);
} else {
ProxyPreparer myIDPreparer =
ConfigurationFactory.getPreparer("activationIdPreparer");

ProxyPreparer mySysPreparer =
ConfigurationFactory.getPreparer("activationSysPreparer");

theActivationID =
(ActivationID) myIDPreparer.prepareProxy(theActivationID);

try {
theActivationSystem =
(ActivationSystem) mySysPreparer.prepareProxy(ActivationGroup.getSystem());
} catch (ActivationException anAE) {
throw new RemoteException("Unable to locate ActivationSystem");
}

ActivationExporter myDefActExp =
new ActivationExporter(theActivationID, myDefaultExporter);

theExporter =
(Exporter) myConfig.getEntry(MODULE_NAME,
"serverExporter",
Exporter.class,
myDefActExp, theActivationID);
}

This code attempts to load the exporter from the config file and, if it fails, uses a default exporter that it's defined previously. This example supports both activation and transient rmi references via the check for a valid activation id. The next step after this is to call export(), of course.

For more details and code examples, see Use net.jini.export.

Clients, Services and Identity

Security is only useful if clients and services are running as identifiable entities. JINI 2.0 relies on the use of JAAS for this purpose. It is expected that clients and services will "login" in some fashion. The means by which a client gains it's identity varies widely depending on whether it's entirely standalone or runs as a GUI on some user's desktop. In the case of the server, one typically allows a deployer to specify a LoginContext in the configuration file. The service then loads it from the configuration file, logs in with it and uses the resultant Subject to perform initialisation:

 // start of method.....
try {
theLoginContext = (LoginContext)
ConfigurationFactory.getEntry("loginContext",
LoginContext.class, null);

if (theLoginContext != null) {
try {
theLoginContext.login();
} catch (LoginException aLE) {
theLogger.log(Level.SEVERE, "Couldn't login", aLE);
throw new ConfigurationException("Login invalid", aLE);
}
}

// Initialisation called here which loads all base classes and starts threads etc - code will now run as the logged in principle.
//
doPriv(new PrivilegedInitImpl());
} catch (Exception anE) {
throw new ConfigurationException("loginContext has insufficient privileges");
}
// rest of method......

private Object doPriv(PrivilegedExceptionAction aPAE)
throws Exception {

if (theLoginContext != null) {
return Subject.doAsPrivileged(theLoginContext.getSubject(),
aPAE, null);
} else
return aPAE.run();
}
For more more information on JAAS and code examples see Java Authentication and Authorization Service (JAAS) in Java 2, Standard Edition (J2SE) 1.4.

Support for Proxy Verification

JINI 2.0 clients are encouraged to verify that a proxy downloaded from an LUS is trustworthy using a ProxyPreparer. Certain classes and forms of remote reference require no special work (see section 3.2.1, "Verifying Trust" in the Overview. For other forms of remote reference or smart proxies, we need to do more work. First, we need to arrange for our server/remote object to implement ServerProxyTrust which returns a suitable verifier object to check our smart proxy with. Here's an example:

 public TrustVerifier getProxyVerifier() throws RemoteException {
return new ProxyVerifier(theStub, theLookupStore.getUuid());
}
And the ProxyVerifier looks like this:
class ProxyVerifier implements TrustVerifier, Serializable {

private RemoteMethodControl theOriginalStub;
private Uuid theOriginalUuid;

/**
Ensures that the passed stub meets the necessary criteria for
TrustVerification. If the stub does not qualify, we throw an
UnsupportedOperationException. This set of tests is necessary due
to the fact that the stub's compliance is determined, in part by
configuration of the appropriate Exporter in the config file.
*/
ProxyVerifier(BlitzServer aServer, Uuid aUuid) {
if (! (aServer instanceof RemoteMethodControl))
throw new UnsupportedOperationException("Server stub does not support RemoteMethodControl - wrong Exporter?");

if (! (aServer instanceof TrustEquivalence))
throw new UnsupportedOperationException("Server stub does not support TrustEquivalance - wrong Exporter?");

theOriginalStub = (RemoteMethodControl) aServer;
theOriginalUuid = aUuid;
}

public boolean isTrustedObject(Object anObject,
TrustVerifier.Context aContext)
throws RemoteException {

RemoteMethodControl myOtherServer;
Uuid myOtherUuid;

/*
One might be tempted to implement all of this by having all proxies
implement a particular interface and obtain the details like that
but it opens the way to a "foreign" proxy implementing the interface
and nothing else such that it passes all our tests but actually isn't
our proxy - thus we test the concrete class.
*/
if (anObject instanceof ConstrainableBlitzProxy) {
ConstrainableBlitzProxy myProxy = (ConstrainableBlitzProxy)
anObject;

myOtherServer = (RemoteMethodControl) myProxy.theStub;
myOtherUuid = myProxy.theUuid;
} else if ((anObject instanceof BlitzServer) &&
(anObject instanceof RemoteMethodControl)) {
myOtherServer = (RemoteMethodControl) anObject;
myOtherUuid = theOriginalUuid;
} else {
// It's nothing we know about - fail it.
return false;
}

if (! theOriginalUuid.equals(myOtherUuid))
return false;

// Get client constraints from passed proxy
MethodConstraints myConstraints = myOtherServer.getConstraints();

// Create copy of original server stub with constraints applied
TrustEquivalence myConstrainedStub =
(TrustEquivalence) theOriginalStub.setConstraints(myConstraints);

return myConstrainedStub.checkTrustEquivalence(myOtherServer);
}

/**
We override this method to check that integrity of the Verifier has
been maintained. There are a number of potential sources of compromise
such as "fiddling" with the serialized steam or a "misbehaving" JVM
implementation.
*/
private void readObject(ObjectInputStream anOIS)
throws IOException, ClassNotFoundException {

anOIS.defaultReadObject();

if ((theOriginalStub == null) || (theOriginalUuid == null)) {
throw new InvalidObjectException("Internal state has been compromised");
}

if (! (theOriginalStub instanceof TrustEquivalence))
throw new InvalidObjectException("Stub doesn't implement TrustEquivalence");
}
}

The key points to notice here are that, when we built our proxy (ConstraintableBlitzProxy), we embedded a Uuid as well as a remote reference in the proxy, which implements ReferentUuid, and so we compare both the stubs and the Uuids of the proxies. Implementing ReferentUuid is relatively "free" in terms of programming cost as we typically generate and save one for the purposes of creating a ServiceId which will be used during the join protocol. For another example, see Outrigger's ProxyVerifier class.

Now we need to provide clients with access to our ServerProxyTrust.getProxyVerifier method. To keep things simple, we'll assume that we use a standard JERI exporter to build our remote reference (things get much more complicated if we use a custom JERI implementation which, for example, uses our own InvocationHandler). With this assumption, we know that an appropriately constructed Exporter will create a remote reference with a trusted method which allows a client to remotely invoke ServerProxyTrust.getProxyVerifier(). A suitable Exporter could be configured thus:

 import net.jini.jeri.tcp.TcpServerEndpoint;
import net.jini.jeri.ProxyTrustILFactory;
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;

serverExporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new ProxyTrustILFactory(null, null), false, true);

Clients gain access to the remote reference's trusted method using an instance of ProxyTrustIterator which is obtained from a specific method on our smart proxy called getProxyTrustIterator:

 private ProxyTrustIterator getProxyTrustIterator() {
return new SingletonProxyTrustIterator(theStub);
}
SingletonProxyTrustIterator is a standard JINI class to which we pass our remote reference (remember the assumption above?) which will call across to the server and invoke the ServerProxyTrust.getProxyVerifier method for us. [Note: All the above is "plumbing", unseen by the client, which simply does:
 /* Lookup proxy in LUS */
Object server = .......

/* Prepare the server proxy */
ProxyPreparer preparer = (ProxyPreparer) config.getEntry(
"org.dancres.example.Client",
"preparer", ProxyPreparer.class, new BasicProxyPreparer());
Thingy preparedServer = (Thingy) preparer.prepareProxy(server);
]

Support for Constraints

Part of the JINI 2.0 security model provides for the setting of constraints (constraints can be applied to clients or services). If you choose to "publish" a simple remote reference into a lookup service, so long as you've used appropriate exporters in the configuration file, you need do nothing. However, if you choose to use an smart proxy, that is one that wraps the remote reference (as Outrigger does) you need to do some more work:
  1. You need to arrange for your proxy to implement RemoteMethodControl and you'll need to write some code to map from methods on the wrapping proxy to those of the remote reference (a good example of this can be found in the outrigger class ConstrainableSpaceProxy). Note that if the methods on the proxy are the same as those of the remote interface of the back-end service, there's much less to do (see the other constrainable proxies in the outrigger package for examples).
  2. As part of exporting a remote reference, your service should test to see if the remote reference generated by the exporter implements RemoteMethodControl and then decide whether to create a proxy which supports RemoteMethodControl or not. There's no point in exporting a proxy which supports RemoteMethodControl if the underlying remote reference doesn't support it (Again, Outrigger does this in OutriggerServerImpl).
For a detailed treatment of RemoteMethodControl, see Implement net.jini.core.constraint.RemoteMethodControl.

Integration With ServiceStarter (com.sun.jini.start)

If you wish to support use of your service with NonActivatableServiceDescriptor, you will need to implement a constructor which has a method signature of (String[], LifeCycle). Where String[] will contain the arguments specified for the service (typically specifying a configuration file).

If you wish to support use of your service with SharedActivatableServiceDescriptor, you will need to implement a constructor which has a method signature of (ActivationID, MarshalledObject) where the MarshalledObject will contain an object of type String[] representing the arguments (typically specifying a configuration file) passed to the service implementation.

In addition, your service should implement ServiceProxyAccessor which should return the proxy your service wishes to export. If you export a smart proxy, return a reference to that proxy otherwise you should return the remote reference created during the export stage.

Finally, if you wish to support activation, your service should implement ProxyAccessor. This method should always return the remote reference of your service whether that reference would be published as is or inside a smart proxy.

Example configuration file for starting a transient service:

start-trans-service.config:

import com.sun.jini.start.ServiceDescriptor;
import com.sun.jini.start.NonActivatableServiceDescriptor;
import com.sun.jini.config.ConfigUtil;
com.sun.jini.start {
static private codebase = ConfigUtil.concat(new Object[] {
"http://", ConfigUtil.getHostName(), ":8081/blitz-dl.jar"});

// Should be updated by installer
//
private static jiniRoot = "/home/dan/jini/jini2_1/lib/";
private static dbLib = "/home/dan/lib/";
private static blitzRoot = "/home/dan/src/jini/blitz/";

static classpath = ConfigUtil.concat(new Object[] {
jiniRoot, "jsk-lib.jar", ":",
jiniRoot, "sun-util.jar", ":", dbLib, "db.jar", ":", blitzRoot,
"lib/blitz.jar"});
private static config = ConfigUtil.concat(new Object[] {
blitzRoot, "config/blitz.config"});
private static policy = ConfigUtil.concat(new Object[] {
blitzRoot, "config/policy.all"});

static serviceDescriptors = new ServiceDescriptor[] {
new NonActivatableServiceDescriptor(
codebase, policy, classpath,
"org.dancres.blitz.remote.BlitzServiceImpl",
new String[] { config }
)};
}

Example configuration files for an activatable service:

start-act-service.config:

import com.sun.jini.start.ServiceDescriptor;
import com.sun.jini.start.SharedActivatableServiceDescriptor;
import com.sun.jini.start.SharedActivationGroupDescriptor;
import com.sun.jini.config.ConfigUtil;

com.sun.jini.start {
//
// Blitz environment
//
private static blitzCodebase = ConfigUtil.concat(new Object[] {
"http://", ConfigUtil.getHostName(), ":8081/blitz-dl.jar"});

// Should be updated by installer
//
private static jiniRoot = "/home/dan/jini/jini2_1/lib/";
private static dbLib = "/home/dan/lib/";
private static blitzRoot = "/home/dan/src/jini/blitz/";


private static blitzPolicy = ConfigUtil.concat(new Object[] {
blitzRoot, "config/policy.all"});

private static blitzClasspath = ConfigUtil.concat(new Object[] {
jiniRoot, "jsk-lib.jar", ":",
jiniRoot, "sun-util.jar", ":", dbLib, "db.jar", ":", blitzRoot,
"lib/blitz.jar"});

private static blitzConfig = ConfigUtil.concat(new Object[] {
blitzRoot, "config/blitz.config"});


//
// Shared Group Environment
//
private static sharedVM_policy = blitzPolicy;
private static sharedVM_classpath = ConfigUtil.concat(new Object[] {
jiniRoot, "sharedvm.jar"});
private static sharedVM_log = "/tmp/sharedvm.log";
private static sharedVM_command = null;
private static sharedVM_options = null;
private static sharedVM_properties = null;
private static sharedVM =
new SharedActivationGroupDescriptor(
sharedVM_policy,
sharedVM_classpath,
sharedVM_log,
sharedVM_command,
sharedVM_options,
sharedVM_properties);

private static blitzDesc = new SharedActivatableServiceDescriptor(
blitzCodebase, blitzPolicy, blitzClasspath,
"org.dancres.blitz.remote.BlitzServiceImpl",
sharedVM_log,
new String[] { blitzConfig },
true /* restart */);


static serviceDescriptors = new ServiceDescriptor[] {
sharedVM, blitzDesc
};

//
// Shared Group
//
private static shared_group_codebase = ConfigUtil.concat(new Object[] {"http://", ConfigUtil.getHostName(), ":8080/group-dl.jar"});
private static shared_group_policy = blitzPolicy;
private static shared_group_classpath = ConfigUtil.concat(new Object[] {
jiniRoot, "group.jar"});
private static shared_group_config = ConfigUtil.concat(new Object[] {
blitzRoot, "config/activatable-group.config"});
private static shared_group_impl = "com.sun.jini.start.SharedGroupImpl";
private static shared_group_service =
new SharedActivatableServiceDescriptor(
shared_group_codebase,
shared_group_policy,
shared_group_classpath,
shared_group_impl,
sharedVM_log, // Same as above
new String[] { shared_group_config },
false);

activatable-group.config:

com.sun.jini.start {
// Use defaults
}

Policy file required by all examples:

policy.all:

grant {
permission java.security.AllPermission "", "";

permission com.sun.rmi.rmid.ExecOptionPermission
"-Djava.rmi.server.codebase=*";

permission com.sun.rmi.rmid.ExecPermission
"/usr/local/java/bin/java";

permission com.sun.rmi.rmid.ExecPermission
"/usr/local/java/bin/java_g";

permission com.sun.rmi.rmid.ExecPermission
"/usr/local/java/jre/bin/java";

permission com.sun.rmi.rmid.ExecPermission
"/usr/local/java/jre/bin/java_g";

permission com.sun.rmi.rmid.ExecOptionPermission
"*";

};

Other Considerations

If your remote reference implements more than one client-visible interface, you should probably wrap your remote reference with a number of custom proxies, each of which implements one of those interfaces. The reason for doing this is to ensure that a client only gets access to the interface they asked for - in the case of a JavaSpace we wouldn't want a TransactionManager to "see" the JavaSpace interface. This allows maximum flexibility in setting of security constraints etc.

You may remember that, in the case of smart proxies, it's suggested that, as part of exporting a remote reference, you check to see if the remote reference implements RemoteMethodControl and create an appropriate proxy. Well, there's a standard pattern to adopt for the coding of such smart proxies. For each proxy there should be a base class which doesn't implement RemoteMethodControl and a subclass which does. You can then select the appropriate proxy to construct based on the result of the test on the remote reference.

© Copyright 2003 Dan Creswell

Getting Started With JINI 2.0