Skip to content

Using Groovy’s ConfigSlurper To Support External Configurations With JEE7′s CDI

Thought I’d investigate how to use Groovy’s very nice ConfigSlurper with JEE7′s Contexts and Dependency Injection for the Java EE Platform.

Others have looked at using JNDI and Properties files for the same purpose (and let’s not forget XML, via Solder: “a library of Generally Useful Stuff ™, particularly if you are developing an application based on CDI”).

Still, I’m a Groovy Geek and would like to re-examine things in my own, imitable, fashion. Besides, ConfigSlurper is a nicer, more functional tool than any of the other alternatives.

Here’s the config file we are going to work with:

config {
    envDependent = 422
    greeting {
        string = 'Cowabunga!'
        stuff = 999
    }
}
more.stuff = 'cow'
environments {
    dev {
        config.envDependent = 888
    }
    test {
        config.envDependent = 644
    }
    prod {
        config.envDependent = 333
    }
}

Standard Groovy goodness here.

Here’s the application entry point:

package cdi

import cdi.config.ConfigSlurperConfiguration
import cdi.config.ConfiguredByConfigSlurper
import org.jboss.weld.environment.se.bindings.Parameters
import org.jboss.weld.environment.se.events.ContainerInitialized

import javax.enterprise.context.ApplicationScoped
import javax.enterprise.event.Observes
import javax.inject.Inject

@ConfigSlurperConfiguration(source = "/Srvr.config")
@ApplicationScoped
class Srvr {
    @Inject
    @ConfiguredByConfigSlurper(key = "config.greeting.string")
    private String greeting

    @Inject
    @ConfiguredByConfigSlurper(key = "config.greeting.stuff")
    private Double val

    @Inject
    @ConfiguredByConfigSlurper(key = "config.envDependent")
    private Integer envDependent

    public void startSrvr(@Observes ContainerInitialized event, @Parameters List<String> parameters) {
        println "${greeting}--val: ${val}"
        println "${greeting}--envDependent: ${envDependent}"
    }
}

It’s not a standard Java/Groovy main class. I could have made one up, but the Weld CDI RI supplies org.jboss.weld.environment.se.StartMain: a nice (event-driven) bootstrapper class-cum-DI container that lets one cut out all that ceremonial stuff.

It should be fairly clear what CDI-related stuff is going on here. The ConfigSlurperConfiguration qualifier attribute defines the config source to use (and possibly the active environment); this can be a URL, of course. The ConfiguredByConfigSlurper qualifier tells CDI which of the available configuration keys should be referenced during value injection.

Both of the CDI annotation definitions are pretty simple. There’s not much to ConfigSlurperConfiguration:

package cdi.config

import javax.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER])
public @interface ConfigSlurperConfiguration {
    String source()
    String env() default ""
}

ConfiguredByConfigSlurper is slightly more interesting:

package cdi.config

import javax.enterprise.util.Nonbinding
import javax.inject.Qualifier
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER])
public @interface ConfiguredByConfigSlurper {
    @Nonbinding String key() default "";
    @Nonbinding boolean required() default true;
}

Note the use of @Nonbinding. Rick Hightower gives the clearest explanation of why this is required over at DZone’s JavaLobby:

@Nonbinding: Required when using an annotation for both injection and configuration.

That’s certainly the case for ConfiguredByConfigSlurper, as you will see.

According to CDI: “A producer method acts as a source of objects to be injected…”

The requisite producer for ConfiguredByConfigSlurper is handled by the imaginatively-named ConfigSlurperProducer class:

package cdi.config

import cdi.events.ConfigSlurperInitEvent

import javax.enterprise.context.ApplicationScoped
import javax.enterprise.event.Observes
import javax.enterprise.inject.Produces
import javax.enterprise.inject.spi.InjectionPoint

@ApplicationScoped
class ConfigSlurperProducer {
    private static final DEFAULT_ENV = System.properties['configslurperproducer.environment']

    private Map flattenedConfig = null

    public  void doInitialise(@Observes ConfigSlurperInitEvent event) {
        def text = this.getClass().getResource(event.source)?.text

        if (!text)
            throw new RequiredConfigNotFoundException("Path=${path}, env=${env}")

        def env = event.environment
        if (!env) {
            env = DEFAULT_ENV
            if (!env)
              throw new RequiredConfigNotFoundException("Path=${path}, env=(UNSPECIFIED)")
        }

        flattenedConfig = new groovy.util.ConfigSlurper(environment: env).parse(text).flatten();
    }

    @Produces
    @ConfiguredByConfigSlurper
    public String getConfigurationString(InjectionPoint ip) { get(ip) }

    @Produces
    @ConfiguredByConfigSlurper
    public Double getConfigurationDouble(InjectionPoint ip) { get(ip) }

    @Produces
    @ConfiguredByConfigSlurper
    public Integer getConfigurationInteger(InjectionPoint ip) { get(ip) }

    // "Duck Typing" FTW!
    private get(InjectionPoint ip) {
        def (required, key) = ip.getAnnotated().getAnnotation(ConfiguredByConfigSlurper).with { a ->
            [a.required(), a.key()]
        }

        if (required && !flattenedConfig.containsKey(key))
            throw new RequiredConfigKeyNotFoundException("'${key}'")

        flattenedConfig.get(key)
    }
}

This nice, concise class is responsible for defining the methods that correspond to the ConfiguredByConfigSlurper qualifier and for ‘driving’ ConfigSlurper accordingly. For each producer method, the InjectionPoint parameter provides access to the specific parameters expressed in the source code. Interestingly, the actual name of the method is irrelevant, as long as it is unique (and nice for us dumb humans to comprehend). There is a separate producer method for each different type of injection point. Groovy’s ‘Duck Typing’ ability makes it easy to keep the class as DRY as possible. A pure Java version of this class would suffer from a fair bit of repetition.

ConfigSlurperProducer requires initialisation at app startup time. As I have written things (and I wrote things this way to deliberately learn how to get ‘cooperating’ annotations going) any initialisation parameters are specified in parameters to the separate ConfigSlurperConfiguration qualifier annotation (which may reference the System properties). At runtime, ConfigSlurperConfiguration does its good stuff and raises an ConfigSlurperInitEvent event. The doInitialise method responds to this event appropriately.

As far as I can see there aren’t very many sources “out there” telling you how to get annotations cooperating with each other like this (ie I couldn’t really find one at all) so pay attention to the next bit, children :-)

There is no Producer class for ConfigSlurperConfiguration. To get cooperating attributes going one needs a CDI Portable Extension and a bit of jiggery-pokery.

package cdi.config.extension

import cdi.config.ConfigSlurperConfiguration
import cdi.config.ConfigSlurperProducer
import cdi.events.ConfigSlurperInitEvent

import javax.enterprise.context.Dependent
import javax.enterprise.event.Event
import javax.enterprise.event.Observes
import javax.enterprise.inject.spi.*
import javax.inject.Inject

@Dependent
class EventExtension implements Extension {

    private String source
    private String env

    @Inject
    Event<ConfigSlurperInitEvent> initEvent;

    public <X> void onProcessAnnotatedType(@Observes @WithAnnotations([ConfigSlurperConfiguration]) ProcessAnnotatedType<X> event) {
        final AnnotatedType<X> type = event.getAnnotatedType()
        type.getAnnotation(ConfigSlurperConfiguration).with { a ->
            source = a.source()
            env = a.env()
        }
    }

    public <X> void onAfterBeanDiscovery(@Observes AfterBeanDiscovery event, final BeanManager beanManager) {
        Bean<ConfigSlurperProducer> bean = (Bean<ConfigSlurperProducer>) beanManager.resolve(beanManager.getBeans(ConfigSlurperProducer));
        ConfigSlurperProducer configSlurperProducer = beanManager.getContext(bean.getScope()).get(bean, beanManager.createCreationalContext(bean));

        beanManager.fireEvent(new ConfigSlurperInitEvent(source: source, environment: env), bean.getQualifiers()[0])
    }
}

Note the two-step process going on here.

First off, the CDI container calls onProcessAnnotatedType (the name is not important but the fact that it observes ProcessAnnotatedType events is). Note how @WithAnnotations restricts the method’s invocations. This ensures that container startup remains as efficient as possible.

In the second step, made after all beans have been discovered, an application-specific ConfigSlurperInitEvent is created with the requisite parameters and fired at the ConfigSlurperProducer.

For completeness, here is the extremely simple ConfigSlurperInitEvent class:

package cdi.events

class ConfigSlurperInitEvent {
  String source
  String environment
}

It is a bit of a shame that all this couldn’t be done in a single step, but such is life. Here is a bit of background on this.

WELD-1682 describes why it is currently necessary to make the extension a Dependent class. In a nutshell: EventExtension would not be treated by CDI as a ‘bean’ without this annotation and “Weld forbids a BeanManager lookup from classes that are not beans.”

Want a demo? Your wish is my command:

Note that I passed “-Dconfigslurperproducer.environment=test” on the command line, so that a particular environment section was selected from the config file.

Good stuff, eh?!

Tags: , ,

C, Java Enterprise Edition, JEE, J2EE, JBoss, Application Server, Glassfish, JavaServer Pages, JSP, Tag Libraries, Servlets, Enterprise Java Beans, EJB, Java Messaging Service JMS, BEA Weblogic, JBoss, Application Servers, Spring Framework, Groovy, Grails, Griffon, GPars, GAnt, Spock, Gradle, Seam, Open Source, Service Oriented Architectures, SOA, Java 2 Standard Edition, J2SE, Eclipse, Intellij, Oracle Service Bus, OSB