configuring jee apps with cdi
Every application has them - configuration parameters: admin credentials, support email addresses, urls for external services, the list is endless. Either in .properties files or in a database, they are hell to keep track of and maintain. This post details a sane way to keep all meta-information on them strictly in the source code by using JSR-299 (CDI).
Goal: we want to maintain all our config params in the source code during development, but still be able to change them during compile- or even runtime later in the application’s lifespan. We will achieve this by using JSR-299 dependency injection:
First we create a @Qualifier to label all our config params with:
@Target(value = { FIELD, METHOD })
@Retention(value = RUNTIME)
@Dependent
@Inherited
@Qualifier
public @interface ConfigValue {
}
We’ll certainly need additional information about the config param: a symbolic name, a default value and a description about how the param is going to be used.
Unfortunately we can’t just add methods to our qualifier-interface as CDI matches producer methods exactly - we cannot use the qualifier to specify more meta-information.
So we have the choice to either create a seperate annotation for each piece of meta-information we want to be able to specify, or just create a single annotation type
and add all needed methods there. Since Java does not allow null as default values in annotations (somehow null is not a ‘constant expression’, whatever…) I decided
to create one annotation for all mandatory data, and then create an annotation for each piece of optional information:
@Target(value = { FIELD, METHOD })
@Retention(value = RUNTIME)
@Inherited
public @interface ConfigParams {
/**
* the {@link ConfigValue}'s default value, always a String, if a non-String
* {@link ConfigValue} is annotated with a preset that cannot be converted
* an exception is thrown in the producer method and 'svn blame' is going to
* be used against you
*/
String preset();
/**
* describes what the {@link ConfigValue} is used for, at this specific
* injection point
*/
String purpose();
}
/**
* specifies an optional symbolic name for a {@link ConfigValue}
*/
@Target(value = { FIELD })
@Retention(value = RUNTIME)
@Inherited
public @interface ConfigKey {
/**
* the symbolic name to be used
*/
String value();
}
ConfigParams holds all mandatory information (default value and a short description on how the param is going to be used),
while ConfigKey is optional and specifies a symbolic name for a ConfigValue.
This is necessary because our producer service chooses the fully qualified classname and fieldname as a default name for a ConfigValue,
and the ConfigKey allows us to use the same ConfigValue at different locations in our project.
Now we just need to write @Produces methods for each annotated type, we’ll just implement Strings for now:
@ApplicationScoped
public class ConfigurationService {
@Inject
private ConfigStore store;
@Produces
@ConfigValue
public String makeConfigString(InjectionPoint injectionPoint) throws ConfigMetaInfoException {
// extract meta information
ConfigParams params = injectionPoint.getAnnotated().getAnnotation(ConfigParams.class);
if (params == null) {
throw new ConfigMetaInfoException("ConfigParams annotation missing at injection point "
+ injectionPoint.toString());
}
// key?
String key = calcDefaultName(injectionPoint);
ConfigKey cfgKey = injectionPoint.getAnnotated().getAnnotation(ConfigKey.class);
if (cfgKey != null) {
key = cfgKey.value();
}
// ask store for value
String rv = store.getConfigValue(key, params.preset());
// log.info(String.format("config %s => %s", key, rv));
return rv;
}
}
The ConfigStore interface defines a store for @ConfigValues, in my current project this is a @Singleton bean that uses
JPA to persist config params to a database. Let’s just use a Hashmap in memory to demonstrate:
/**
* default {@link ConfigStore} implementation, stores config values locally in a
* HashMap, does not persist changes anywhere but in memory
*
* @author nsn
*
*/
@Dependent
@Default
public class MockupConfigStoreImpl implements ConfigStore {
private HashMap<String, String> values;
/**
* initializes the values map
*/
@PostConstruct
public void init() {
values = new HashMap<String, String>();
}
@Override
public String getConfigValue(String key, String preset) {
if (values.containsKey(key)) {
return values.get(key);
}
values.put(key, preset);
return preset;
}
}
A real ConfigStore would probably need to provide a getAllValues() method that returns all ConfigValues, each with all their injection points, default value
and meaning meta-information. This method could then be used to create a simple web-frontend to actually set the config params.
This is the way my team and I manage our current project’s configuration, and so far we like it a lot more than the .properties files we used for the Wizard101 website.
comments powered by Disqus
RSS feed