Jini 2.0 offers many new tools to the developer. Not only do these tools help you build highly secure distributed systems, but they also make developing and deploying Jini systems easier.
Before illustrating the key new Jini 2.0 tools, I'd like to mention that you do not need to change your pre-2.0 services' code to run those services on the latest version of the Jini starter kit: You can run those services unmodified by simply adding jar files to your classpath and JDK ext
directory.
The most important reason to take advantage of the new Jini tools is to add security to your services. Every aspect of securely deploying and using a Jini service is configurable in Jini 2.0. That includes your choice of a communication protocol, including protocols based on a new, security-conscious implementation of RMI, Jini Remote Method Invocation (JERI). Because the level of security your services need often depends on the environment those services run in, security-related attributes are not hard-coded into Jini service implementations and clients. Rather, Jini 2.0 defines a configuration mechanism to assign security features to Jini services at deployment time. This article provides a tutorial on the new Jini configuration framework. While the primary use cases for classes in the net.jini.config
package are related to configuring security, the framework is flexible enough to configure any aspect of a Jini service. You may prefer to use that new model to configure all runtime attributes of your services instead of relying on ad-hoc property files and system properties.
The 2.0 release of Jini introduced a number of new service aspects that should be determined at deployment time. These include the implementation of Java RMI to use, the constraints to employ when making remote calls, the transports and protocols to use for discovery, or what JAAS subject to log in as. These configuration items are best represented by non-trivial objects that would be hard to derive from simple string properties and command-line arguments.
Version 2.0 of the starter kit introduces a new model for addressing this issue: net.jini.config
, which I will henceforth refer to as the config model. The config model allows a developer to determine what aspects of a Jini service are controlled by the person deploying the module. The developer can define Java objects for those configuration options. Those objects are then loaded into a Jini system at runtime.
Configuration
The Configuration
class serves as a medium of conversation between a Jini service developer and the person deploying, or using, that software. The developer delegates certain aspects of the service to the deployer. The deployer then decides what values those objects should assume to achieve the goals of a particular deployment situation. The Configuration
implementation provides the deployer with a set of tools to construct those objects.
That division of responsibilities is a good fit for an object-oriented (OO) language such as Java. The developer can use the Java type system to model the contract needed from each object, while the deployer gets to use the polymorphism of the Java programming language to construct instances that both implement the contract and meets the needs of a given deployment.
Configuration objects are created at runtime by an instance of the net.jini.config.Configuration
interface from configuration entries. A developer defines the requirements each configuration entry must meet. A Configuration
instance is then responsible for evaluating configuration entries to yield the objects that are �plugged into� the runtime Jini system. The deployer chooses what Configuration
implementation to use, and defines the configuration entries. The config model, therefore, gives the deployer control over what object each configuration entry will yield.
The default implementation of Configuration
is ConfigurationFile
. That class allows for the construction of arbitrary Java objects based on a text file interpreted at runtime and defined in a Java-based language. The following section explains that Java-based configuration language, and provides examples of its use.
The following code snippet illustrates configuration-specific additions to a Jini service's code:
package org.jini.user.jmcclain.myservice; import net.jini.config.Configuration; import net.jini.config.ConfigurationProvider; import net.jini.config.ConfigurationException; import net.jini.config.NoSuchEntryException; .... class MyServiceImpl { private MyServiceImpl(String[] argv) throws ConfigurationException { .... Configuration config = ConfigurationProvider.getInstance(argv, getClass().getClassLoader()); ....
A client would require similar code. Utilities that rely on a configuration object generally consume those configuration objects in their constructors.
The ConfigurationProvider
class provides static methods that can be used to create a configuration. By delegating to ConfigurationProvider
, we allow the deployer to use whatever configuration implementation he desires. By default ConfigurationFile
will be used, but using Java's resource mechanism, deployers can arrange for another implementation to be used.
The argv
String array is passed into ConfigurationProvider
. How that array is parsed will vary between Configuration
implementations. ConfigurationFile
treats the first element as a URL to look for a ConfigurationFile
source file. Any additional elements in the array override the contents of the source file. This example assumes that all of the program's command-line arguments are used to obtain the configuration. ConfigurationFile
's override feature allows for simple values to be provided on the command line without having to edit the configuration source.
Conceptually, a Configuration
instance is composed of configuration entries. When a program needs an object to be provided by the deployer, that program asks Configuration
to evaluate one of those entries to obtain an object. Evaluating a configuration entry twice may or may not yield the same object. A service's developer must document what configuration entries her service uses, when those entries are evaluated, the requirements of those entry objects, and how those objects are used.
Configuration entries are identified by two strings: one denoting the component and one the name. The config model expects that every configuration entry used by a given software module (e.g. a client, service, or utility) shares the same component string, but has a distinct, hopefully descriptive, name string. In effect, the component string forms a name space and prevents collisions if two modules happen to use the same name. In Sun's contributed implementation of the JSK, the package name serves as the component string for clients and services, and the class name as the component string for utilities.
Once we have a Configuration
, we can retrieve entries from it. Suppose MyService
has a daemon thread that cleans up internal data structures, and we want to give the deployer control over that thread's priority. The following code snippet illustrates how a deployer might determine that daemon thread's priority:
.... int reapingPriority = ((Integer)config.getEntry( "org.jini.user.jmcclain.myservice", // component "reaperPriority", // name int.class, // type new Integer(Thread.NORM_PRIORITY)) // default value ).intValue(); if ((reapingPriority < Thread.MIN_PRIORITY) || (reapingPriority > Thread.MAX_PRIORITY)) { throw new ConfigurationException("entry for component " + "org.jini.user.jmcclain.myservice, name reaperPriority " + "must be between " + Thread.MIN_PRIORITY + " and " + Thread.MAX_PRIORITY + ", has a value of " + reapingPriority); } reaperThread = new ReaperThread(); reaperThread.setPriority(reapingPriority); reaperThread.setDaemon(true); reaperThread.start(); ....
The above example illustrates some important configuration principles:
MyService
does not need to provide a value for every configuration entry. Since default values reduce the burden on the deployer, a default should be provided whenever possible. When getting the value of a configuration entry, the caller can also specify the type of object expected of that entry.int
, not an Integer
. However, because getEntry()
returns an Object
, the return value is wrapped in Integer
, as is the default value.int
), we don't need to check for null
.ConfigurationException
. We would also want to log such a problem, but the logging code has been removed for clarity.A ConfigurationFile
source file might look as follows:
org.jini.user.jmcclain.myservice { reaperPriority = 6; }
If this file were named myservice.config
, the command line might look like this:
java -Djava.rmi.server.codebase=http://recycle/myservice-dl.jar \ -jar myservice.jar \ myservice.config
In this case, the reaping thread would have a priority of 6. Because we provided a default when fetching reaperPriority
, we could use an empty file:
java -Djava.rmi.server.codebase=http://recycle/myservice-dl.jar \ -jar myservice.jar \ empty.config
or no file:
java -Djava.rmi.server.codebase=http://recycle/myservice-dl.jar \ -jar myservice.jar
and still have a running service. In both cases, the reaping thread would have a priority of Thread.NORM_PRIORITY
. We could also use a command-line override to obtain the effect of using myservice.config
without specifying a file on the command line:
java -Djava.rmi.server.codebase=http://recycle/myservice-dl.jar \ -jar myservice.jar \ - org.jini.user.jmcclain.myservice.reaperPriority=6
Passing '-' as the first command-line argument to MyService
indicates that there is no ConfigurationFile
source file. Replacing '-' with empty.config
would have a similar effect.
Finally we could use myservice.config
but use an override to give the reaping thread some value besides 6:
java -Djava.rmi.server.codebase=http://recycle/myservice-dl.jar \ -jar myservice.jar \ myservice.config org.jini.user.jmcclain.myservice.reaperPriority=4
In this case the reaping thread would have a priority of 4.
While we could use Configuration
to retrieve only simple primitive values, the config models power is more evident when we retrieve complete objects from a Configuration
.
For instance, MyServiceImpl
will need to find lookup services to register with. The service could obtain an array of group names and lookup locators from the configuration and use those to create a LookupDiscoveryManager
. Alternatively, or we could specify an entire DiscoveryManagement
object:
DiscoveryManagement discoveryManager; try { discoveryManager = (DiscoveryManagement)config.getEntry( "org.jini.user.jmcclain.myservice", // component "discoveryManager", // name DiscoveryManagement.class) // type if (null == discoveryManager) { throw new ConfigurationException("entry for component " + "org.jini.user.jmcclain.myservice, name " + "discoveryManager must be non-null"); } } catch (NoSuchEntryException e) { // default value discoveryManager = new LookupDiscoveryManager( new String[] {""}, null, null, config); }
A ConfigurationFile
source file that provided a value for discoveryManager
might look something like this:
import net.jini.core.discovery.LookupLocator; import net.jini.discovery.LookupDiscoveryManager; org.jini.user.jmcclain.myservice { discoveryManager = new LookupDiscoveryManager( new String{"pr.bigcorp.com", "mkt.bigcorp.com"}, new LookupLocator("recycle.bigcorp.com", 4160), null, this); // the current config }
Using the above configuration source file, discoveryManager
would end up with a LookupDiscoveryManager
that would discover lookups in the pr.bigcorp.com
and mkt.bigcorp.com
groups plus any lookups on host recycle.bigcorp.com
using port 4160.
A few things to note about this example:
discoveryManager
, we throw a ConfigurationException
if we get null
back. That strategy is better than just accepting the default, since a null
value here likely reflects an error in the configuration source file.NoSuchEntryException
(which is what getEntry
throws if it can't find a configuration entry with the specified component and name) and then create the default only if necessary. We do this in cases where creating the default can be very expensive or has a lot of side effects. Creating a LookupDiscoveryManager
has the potential to open sockets, start off threads, etc., so we only create the default once we know we need it. If there was no reasonable default for the discoveryManager
configuration entry, we would have propagated NoSuchEntryException
.config
parameter into the constructor for the default LookupDiscoveryManager
. That allows the LookupDiscoveryManager
to use the Configuration
to control implementation-specific aspects, such as what discovery protocols to use.By retrieving a complete DiscoveryManagement
object from the configuration, MyService
gives the deployer a lot of control over what lookups the service will register with. The deployer can provide:
LookupDiscoveryManager
pre-set with the appropriate groups and locators, orDiscoveryManagement
implementation that finds lookups in LDAP servers, orDiscoveryManagement
implementation that finds lookups in other lookups, orDiscoveryManagement
implementation that reads lookup proxies out of a shared network file systemAs these two examples show, a configuration may contain errors: it may be missing entries that don't have defaults, or it could have values for all the entries, but one or more of them could be malformed. For that reason it is generally best for a module to retrieve all of its configuration entries early, usually in the constructor, so errors can be signaled early. That makes it easier for the deployer to debug her configuration.
That strategy should be followed even if the values of these entries are not going to be used immediately, or may not be used at all. If you know that a given entry is never going to be used (e.g. a persistence directory for a transient service) then skipping it is probably the right thing to do. Deferring the fetching of an entry may also be the right approach if the entry in question is expensive to create.
Once the service got hold of a DiscoveryManagement
object, it can create its JoinManager
. Ideally, instead of obtaining a DiscoveryManagement
object from the configuration, the service would have retrieved JoinManager
. However, JoinManager
is not a good candidate for a Configuration
entry. Unlike the DiscoveryManagement
object, where the deployer is expected to have all the information to construct an instance, constructing a JoinManager
requires both information the deployer will have (e.g. DiscoveryManagement
object, LeaseRenewalManager
, some of the attributes), as well as objects only the service knows about (e.g. the proxy, other attributes, ServiceID/ServiceID listener). As a result, the MyService
implementation uses the following code:
joinManager = new JoinManager(proxy, getAttributes(), serviceID, discoveryManager, null, config);
That above code passes the Configuration
into JoinManager
's constructor. What JoinManager
does with that Configuration
is application-specific. The JoinManager
in the Sun-contributed JSK uses that object to obtain proxy preparers, a thread pool, a LeaseRenewalManager
(since null
was passed in for the leaseMgr
parameter), a wakeup manager, and the number of retries to perform on various tasks. Had null
been passed in for the discoveryMgr
parameter, JoinManager
would also obtain a DiscoveryManagement
object from the Configuration
.
The article's final example summarizes the key config model capabilities. All of the Sun-contributed Jini 2.0 Starter Kit service implementations have persistent modes . They also offer administrative proxies that implement JoinAdmin
. JoinAdmin
's methods allow clients to set the groups and locators the service must use for discovery. The services' administrative proxies expect those changes to be part of the service's persisted state.
JoinAdmin
also lets clients change the attributes a service registers with. Again, any changes to the set of attributes must persist. The service also needs an initial set of groups, locators, and attributes. The obvious place to get these values from is the configuration.
The entries for the initial groups, lookup locators, and attributes should only be read the very first time a service instance starts. If the service instance crashes and then restarts, the service should use the persisted values for groups, locators, and attributes.
In addition, lookup discovery object has to implement DiscoveryGroupManagement
and DiscoveryLocatorManagement
in addition to DiscoveryManagement
. Otherwise, the implementations would be unable to implement the methods of JoinAdmin
that change the sets of groups and locators.
Finally, because most of the time the sets of groups and locators to use for discovery must be recovered from the service's persistent store, the DiscoveryManagement
object obtained from the configuration should start with empty sets of groups and locators.
As a result, the Sun-contributed service implementations use the following sort of code for creating a JoinManager
:
private DiscoveryManagement discoveryManager; private JoinManager joinManager; .... void join(Configuration config, Object service) throws IOException, ConfigurationException { // Get a non-null value for DiscoveryManagement instance and // make sure it implements DiscoveryGroupManagement and // DiscoveryLocatorManagement try { discoveryManager = (DiscoveryManagement)config.getEntry( "org.jini.user.jmcclain.myservice", "discoveryManager", DiscoveryManagement.class) if (discoveryManager == null) { throw new ConfigurationException("entry for component " + "org.jini.user.jmcclain.myservice, name " + "discoveryManager must be non-null"); } } catch (NoSuchEntryException e) { discoveryManager = new LookupDiscoveryManager( new String[] {""}, null, null, config); } if (!(discoveryManager instanceof DiscoveryGroupManagement)) throw new ConfigurationException("Entry for component " + "org.jini.user.jmcclain.myservice, name discoveryManager" + "must implement net.jini.discovery.DiscoveryGroupManagement"); if (!(discoveryManager instanceof DiscoveryLocatorManagement)) throw new ConfigurationException("Entry for component " + "org.jini.user.jmcclain.myservice, name discoveryManager" + "must implement " + "net.jini.discovery.DiscoveryLocatorManagement"); // Ensure that discoveryManager is initially set to no groups no // locators final String[] toCheck = ((DiscoveryGroupManagement)discoveryManager).getGroups(); if (toCheck == null || toCheck.length != 0) throw new ConfigurationException("Entry for component " + "org.jini.user.jmcclain.myservice, name discoveryManager " + "must be initially configured with no groups"); if (((DiscoveryLocatorManagement)dgm).getLocators().length != 0) throw new ConfigurationException("Entry for component " + "org.jini.user.jmcclain.myservice, name discoveryManager " + "must be initially configured with no locators"); // if this is the first incarnation, consult config for groups, // locators and attributes. String[] groups; LookupLocators locators; Entry[] attributes; ServiceID serviceID; // noPersistentState() returns true if there is no existing store, // implying that this is first incarnation of this service instance. if (noPersistentState()) { // No state, get initial values from configuration groups = (String[])config.getEntry( "org.jini.user.jmcclain.myservice" "initialLookupGroups", String[].class, new String[]{""}); // the "public" group locators = (LookupLocator[])config.getEntry( "org.jini.user.jmcclain.myservice" "initialLookupLocators", LookupLocator[].class, new LookupLocator[0]); if (locators == null) { throw new ConfigurationException("entry for component " + "org.jini.user.jmcclain.myservice, name " + "initialLookupLocators must be non-null"); } final Entry[] cAttrs = (Entry[])config.getEntry( "org.jini.user.jmcclain.myservice" "initialLookupAttributes", Entry[].class, new Entry[0]); if (cAttrs == null) { throw new ConfigurationException("entry for component " + "org.jini.user.jmcclain.myservice, name " + "initialLookupAttributes must be non-null"); } // stdAttributes() returns ServiceType and ServiceInfo // attributes for this service Entry[] baseAttributes = stdAttributes() if (cAttrs.length == 0) { // No attributes from config, just use standard attributes attributes = baseAttributes; } else { // Combine attributes from config with standard attributes attributes = new Entry[cAttrs.length + baseAttributes.length]; System.arraycopy(baseAttributes, 0, attributes, 0, baseAttributes.length); System.arraycopy(cAttrs, 0, attributes, baseAttributes.length, cAttrs.length); } serviceID = newServiceID() // Calc random ServiceID } else { // Recover pervious state groups = getGroupsFromStore(); locators = getLocatorsFromStore(); attributes = getAttributesFromStore(); serviceID = getServiceIDFromStore(); } ((DiscoveryGroupManagement)discoveryManager).setGroups(groups); ((DiscoveryLocatorManagement)discoveryManager).setLocators(locators); joinManager = new JoinManager(service, attributes, serviceID, discoveryManager, null, config);
While you don't have to use the net.jini.config
package to write a Jini service or client, it is a powerful tool for writing applications that adapt to their environment. The Sun-contributed service implementations and utilities in the JSK use it extensively. The config model also eases the task of configuring the RMI implementation a service uses, paving the way for a pluggable RMI framework with comprehensive security features.
Have an opinion? Readers have already posted 3 comments about this article. Why not add yours?
-
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.