Configuration classes with Dependency Injection for Java
Benjamin Leitner
Configurable<T>
is a bucket-like class that simply holds a value.
The value can be updated either by directly setting a new Object
value or by setting a String
value that is parsed into an Object
of the correct type.
They can have a default value or not, but in the latter case a Class
object must be provided so the Configurable can have information about
its type:
Configurable.value("some_string"); // A Configurable<String> with default value "some_string".
Configurable.noDefault(String.class); // A Configurable<String> with null default value.
Configurables can also be made writable only until they are read, for use as Flags. To do this, use:
// A Configurable<String> with default value "some_string"
// After reading the value, attempting to set it will throw an exception.
// Setting the value to something else prior to calling get() is allowed.
Configurable.flag("some_string");
// Similar, but the default value is null.
Configurable.noDefaultFlag(String.class);
For classes other than primitives and strings, a Parser must be provided so the Configurable can translate new string values into objects.
// A Configurable for a custom class
Configurable.<MyClass>builder()
.withDefaultValue(myInstance)
.withParser(myClassParser) // A com.google.common.base.Function<String, MyClass>
.build();
You can also register listeners on Configurables to be updated on changes made elsewhere:
ListenerRegistration configurable.registerListener(new ConfigChangeListener<[T]>() {
void onConfigurationChange(T newValue) {
// I care about this change, I should really do something.
}
}
where T
should be replaced by the configurable's type. The ListenerRegistration
is a handle
that allows for unregistering the listener if you later do not want to listen anymore. If you want
the listener to get an immediate update with the current value upon registration, call:
configurable.registerListener(listener, true /* Tell the listener right now what the value is. */);
The real magic comes into play when using Configurables with Dependency Injection. Adding the
annotation @Config
to a configurable tells the included annotation processor to process the
Configurable field. The annotation processor generates modules for both the Dagger and Guice
DI frameworks.
How the @Config
-marked Configurables are grouped into modules depends on their visibility.
public and private @Config
fields are pulled up into a root Module class in the lowest package
that includes all subpackages with at least one @Config
-marked Configurable. Package-local fields
are included in modules in the same package, and these modules are linked to the root module.
Reflection is used for building the bindings to private Configurables. If you wish to avoid the costs of reflection, make sure your Configurables have at least package-local visibility.
The annotation processor will write one or more modules, with a Root module appearing in the highest
level package that contains all @Config
-marked configurables itself or in subpackages.
By default, the modules will bind the configuration value to an @ConfigValue([name])
annotation,
where name
is the value given in the @Config
annotation, if present, or the field name of the
Configurable otherwise. Note that these bindings must be globally unique. To change these
bindings, a @Qualifier
(for Dagger) and/or @BindingAnnotation
(for Guice) can also be placed
on the Configurable field. If one is found, the binding is replaced in the corresponding DI module.
That is, if a @Qualifier
annotation is found, the binding is overridden in Dagger. If a
@BindingAnnotation
is found, the binding is overridden in Guice. These potential overrides are
independent. To allow for use of either, use an annotation that is both a @Qualifier
and a
@BindingAnnotation
(like @ConfigValue
).
To use Guice for dependency injection, include in your Injector
creation both the
MainConfigGuiceModule
and any generated ConfigGuiceModule
s needed e.g:
public static void main(String[] args) {
Injector injector = Guice.createInjector(
MaingConfigGuiceModule.create(),
new path.to.package.one.ConfigGuiceModule(),
new some.other.library.path.ConfigGuiceModule(),
...
);
injector.getInstance(...);
}
You can then include @ConfigValue([name])
-annotated parameters in your constructors and they will
be filled appropriately.
To use Dagger for dependency injection, create a Component that includes MainConfigDaggerModule
and any generated ConfigDaggerModule
s needed e.g:
@Component(modules = {
MainConfigDaggerModule.class,
path.to.package.one.ConfigDaggerModule.class,
some.other.library.path.ConfigDaggerModule.class})
public interface ConfigEnabledComponent {
}
and then build it as follows:
public static void main(String[] args) {
ConfigEnabledComponent component = DaggerConfigEnabledComponent.builder()
.mainConfigDaggerModule(MainConfigDaggerModule.create())
.build()
}
- You need not install all of the modules (if more than one) generated from one annotation processor run. The modules generated at one time will have a single root module that includes all the others.
- If you are not using Configurables as flags and expect the values may change, it is highly
recommended to inject
@ConfigValue([name]) Provider<[Type]>
rather than@ConfigValue([name]) Type
.
The initial values for Configurables can also be set from the command line (or from any list of strings). To do this, in the code above simply replace:
MainConfig(Guice|Dagger)Module.create()
with:
MainConfig(Guice|Dagger)Module.forArguments([args])
where [args]
can be a list of strings or a varargs String array. The rules of processing are:
- Each string represents a single config assignment.
- An assignment has the form
--[config_name]=[config_value]
. The config name can be either thename
as defined above, or the FQPN of the field can be used. If more than one config has the samename
, then FQPN must be used. - For boolean configs, the
=[config_value]
may be omitted. Including--[config_name]
will set the value totrue
and--no[config_name]
will set it tofalse
. - If a string
--
is encountered, all further processing of values terminates. - There are three special config names that indicate other places to load config values
config_file
- the value for this argument is interpreted as a file from which to read more config name-value pairs.config_resource
- the value is treated as the URL of a Resource from which to read more config name-value pairs.system_config
- the value is a comma-separated list of names of System Properties from which to read values.
If using injection, a @Singleton Configuration
class becomes available for injection.
The Configuration
class exposes a number of methods for interacting with configurables. It
allows for getting/setting configurable values by name. It also works with the
ConfigObjectWriter
and ConfigStringWriter
classes to support exporting all current config
information (e.g. for persistence elsewhere).