|
|
|
Spontaneous Networking |
Jini Community News |
Discuss |
Print |
Email |
Screen Friendly Version |
Previous |
Next
|
|
Sponsored Link •
|
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.
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, or DiscoveryManagement implementation that finds
lookups in LDAP servers, or DiscoveryManagement implementation that finds
lookups in other lookups, or DiscoveryManagement 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.
Discuss this article in the Articles Forum topic, Using Objects to Configure Jini Network Services.
|
Sponsored Links
|