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.
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: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.
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 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:
RMIC
to generate stubs and skeletons.RMIC.Endpoint
instances as parameters to their constructors which provides the means
for the configuring a number of different transports including:
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;The service can then use the
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);
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.
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.....For more more information on JAAS and code examples see Java Authentication and Authorization Service (JAAS) in Java 2, Standard Edition (J2SE) 1.4.
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();
}
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);
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).OutriggerServerImpl).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
"*";
};
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