Context-Specific Configuration in Java
A conundrum that repeatedly appears across all areas of software architecture is how to provide configuration information to those parts of the program that need it. Ideally the root module that loads or otherwise creates the configuration information would pass the information to the needed area. For example, MyApplication
may load MyApplicationConfiguration
and pass that to MyDialog
via its constructor, allowing the dialog to tailor its information based upon the application's current settings.
Often, however, the configuration is so pervasive or so far removed from its module of origin that getting the configuration information to the correct place involves a tedious string of configuration passing. The naive solution is to make the configuration class a singleton, so that any module can get the current configuration by a call to MyConfiguration.getInstance()
. Java uses this approach with the system locale: any application at any time can access the default local by calling the static method Locale.getDefault()
.
While this is certainly an easy option, it is also dangerous. To begin with, it allows access to the configuration at any time. This raises the possibility that the configuration can be accessed before it is appropriately initialized. In the case of Java's default Locale
, this is not so much of an issue, as you can rely on the JVM to set the default locale before the application starts. For custom configuration information this problem more likely to arise, and I have personally seen such a situation cause bugs in software created by my client's software team.
Another problem with using what is effectively a global configuration variable is that it allows access to the configuration from any place. It draconianly forces a single configuration to be used by all modules, not only in the application, but across the entire JVM. An example of the restrictions this causes is in the Java application framework governed by JSR 296. The application framework bases all resource lookups by the single global JVM default locale. This default locale can be changed, but doing so changes the locale for all code. JSR 296 therefore does not allow for the possibility of multiple applications running in the same JVM under different locale.
The list of use cases that encounter this dilemma is long, and the examples fundamental. It would be nice to configure resource lookup so that the same could could look up resources differently depending on the context in which they were called. Logging is commonly currently configurable on a per-class basis, but sometimes the same class should be logged differently based upon which thread is executing. And the issue comes to a head in a framework like Guise™, which runs multiple applications and multiple sessions per application, all within the same JVM. Different Guise application should be able to configure caching, for instance, to function differently in different Guise applications, even for code that isn't aware of the Guise framework.
I've created a solution to this problem after recognizing that areas of local configuration can be grouped based upon the thread in which they are running. Java has the concept of a ThreadGroup
for grouping threads. Guise partitions its code into distinct applications and already runs each of those applications in separate thread groups. By associating configuration information with individual thread groups, it was possible to create a generalized framework in which any module can ask for configuration information, and that configuration information will be provided to it: either a local configuration configured specifically for the current thread group, or a global default configuration.
This configuration framework is based around the com.globalmentor.config.Configurator class. A particular subsystem can create a configuration class that implements Configuration. The controlling application or framework, such as Guise, can store that framwork in a thread group that implements ConfigurationManaged—for example, ConfigurationManagerThreadGroup. Then the framework can spawn the particular module in a thread that is part of this configuration managed thread group. The module can request its configuration at any time in a context-independent way, knowing that it will receive the correct configuration, whether it is running in a configuration managed thread group or not.
Setting up the configuration is the hardest part, although Guise now does this automatically. (Its per-session configuration managed thread groups automatically look up configurations in the Guise session and then in the Guise application.) Once the configurations are set up, accessing configuration information from consuming code could hardly be easier. Let's take a look at how a MarmotConfiguration is accessed. Marmot™ is the GlobalMentor library for accessing resources from a variety of sources, from WebDAV to file systems, via a consistent API. Marmot allows a MarmotResourceCache to be configured for caching resources derived from Marmot repositories. Using the configurator framework described here, a Marmot cache can be retrieved through the MarmotConfiguration like this:
MarmotConfiguration config=Configurator.getConfiguration(MarmotConfiguration.class);
MarmotResourceCache<?> cache=config.getCache();
Retrieving a particular configuration is that simple. In this case, Marmot configures a default cache for the entire JVM that is used in those cases in which no local Marmot configuration has been defined. Here is the code from the main Marmot class:
static
{
Configurator.setDefaultConfiguration(new DefaultMarmotConfiguration());
}
Now any consumer that wants the configured Marmot can ask for the Marmot configuration using the code above. But the marmox.net application, which runs under Guise, uses its own cache for Marmot resources, using a cache directory based upon user owner of the Marmot repository, and creating special cache files for different resource aspects such as thumbnails. How can the Marmot cache be configured specially for the marmox.net application without affecting the same code when it runs under different Guise applications on the same JVM?
The marmox.net application, MarmoxApplication, creates its specialized Marmot cache and sets it as the configuration in the Guise application:
resourceCache=new MarmoxResourceCache(this);
setConfiguration(new DefaultMarmotConfiguration(resourceCache));
As explained earlier, Guise partitions its application sessions into separate thread groups. The Guise session Thread group implements ConfigurationManaged, and when the Configurator asks for a Marmot configuration for the current thread group, the Guise session thread group first searches for a configuration in the current session, and then in the current application.
Even if you aren't using Guise, you can specify different configurations for different contexts as long as you can run those contexts in separate thread groups. Provide your configuration using a thread group that implements ConfigurationManaged; the choice easiest to use is probably ConfigurationManagerThreadGroup. Here is how you would provide MySpecialConfiguration
locally to code in MyRunnable
:
Configuration specialConfig=new MyConfiguration("special info");
ThreadGroup threadGroup=new ConfigurationManagerThreadGroup("my thread group", specialConfig);
Runnable runnable=new MyRunnable();
new Thread(threadGroup, runnable).start();
You can also specify a default configuration for all other code that hasn't been given special configuration:
Configuration defaultConfig=MyConfiguration("default info");
Configurator.setDefaultConfiguration(defaultConfig);
Now any code can request a configuration without needing to know in which context it is running:
MyConfiguration config=Configurator.getConfiguration(MyConfiguration.class);
Code running in the special thread group will receive a configuration with "special info"
. All other code will receive a configuration with "default info"
.
The Configurator
framework described here is a flexible, powerful, and easy-to-use system for setting up configuration information for modules. Program code can request configuration information in a context-independent way that is agnostic to any particular application framework. Configuration information can be specified in some decoupled location for particular software execution contexts, completely transparent to the configuration consumer. The source code is available in the com.globalmentor.config package in the GlobalMentor Subversion repository.