← back to Rants
RSS feed

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.

by nsn on 2011-07-11 tags: programming jee java cdi

comments powered by Disqus