Using Objects to Configure Jini Network Services
by John McClain
June 29, 2004

Summary
Secure large-scale Java distributed computing is at the heart of the 2.0 Jini release. Every aspect of building a secure distributed computing environment is configurable in Jini. However, that freedom introduces a new set of choices Jini developers and service deployers must make. To ease that complexity, Jini 2.0 introduces a service configuration model. Instead of the configuration language du jour - XML - the Jini model uses Java objects to configure complex services and their clients. This article provides a tutorial on using the Jini service configuration model, and also illuminates the decisions by the Jini architects to rely on objects, rather than on a document model, for service configuration.
Use Objects to Configure Jini Network Services Summary

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.

Runtime Configuration of Jini Services

A normal Jini service or client has a number of aspects that are best determined at deployment time. I use "aspects" here in the traditional English sense, not referring to aspect-oriented programming. By deployment time I mean the time when the service/client is run, instead of the time of application development. These aspects include the groups and locators to use, where to persist state, what lease durations should be granted, and what codebase to use. Prior to Jini 2.0, these settings were controlled by a number of ad-hoc mechanisms, including command-line arguments 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.

Using 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.

Configuration Entries

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:

  • The code specifies a default value for a configuration entry. That way, the deployer writing a configuration for 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.
  • The service asks for an int, not an Integer. However, because getEntry() returns an Object, the return value is wrapped in Integer, as is the default value.
  • Because we ask for a non-reference type (an int), we don't need to check for null.
  • We check to make sure the value is sensible, and reflect any misconfiguration as a 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.

Object-Oriented Service Configuration

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:

  • Because the service is retrieving an object and not a primitive type (e.g. int.class), it needs to check that the result is non-null.
  • Since the service does need a 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.
  • Instead of using the defaulting feature, the service catches 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.
  • The service passes a 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:

  • A LookupDiscoveryManager pre-set with the appropriate groups and locators, or
  • An instance of a DiscoveryManagement implementation that finds lookups in LDAP servers, or
  • An instance of a DiscoveryManagement implementation that finds lookups in other lookups, or
  • An instance of a DiscoveryManagement implementation that reads lookup proxies out of a shared network file system

Configuration Strategies

As 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.

Configure Service Administration

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);


Summary

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.

Talk back!

Have an opinion? Readers have already posted 3 comments about this article. Why not add yours?

About the author

-