Tears of a Unicorn Logo

Tears of a Unicorn

Configuring Apache Deltaspike through environment variables

One of the tenants of the 12Factor application methodology and something I liked about Ruby on Rails projects was the way an application could be configured using environment variables. In particular, applications would have a per-environment configuration (development, test, production, etc). In the development configuration, you could tailor the application to focus on a local singular developer, such as using an embedded database and messaging queue rather than the central, common, highly-available database and redundant queue setup.

The accompanying source code for this article can be found here: https://github.com/tearsofaunicorn/deltaspike-environment-config

For Java applications using CDI, Apache Deltaspike supports a similar approach through its Project Stages. Using specific annotations against components, you can configure which services should be able in specific environments.

For example, in a production environment we want to use a reliable, asynchronous email send queue while in development we just want to send emails straight out:

 /**
 * Implementation of a Mailer which directly send the mail out
 */
@Exclude(ifProjectStage = {ProjectStage.UnitTest.class, ProjectStage.Production.class})
public class DirectMailer implements Mailer {
    @Override
    public void sendMail(Mail mail) {
        System.out.println("Sending mail directly out to recipient");
    }
}
/**
 * Implementation of a Mailer which sends the mail out via an asynchronous queue
 */
@Exclude(exceptIfProjectStage = ProjectStage.Production.class)
public class AsyncMailer implements Mailer {
    @Override
    public void sendMail(Mail mail) {
        System.out.println("Placing mail on queue to be sent out later");
    }
}

You can see in the above example, through the @Exclude annotation, the DirectMailer does not apply when running in a Production or UnitTest ProjectStage while the AsyncMailer is excluded in every environment except the Production one.

At CDI container bootstrap DeltaSpike will read a system property in order to set which ProjectStage is active. In this case, the Development Project Stage would be configured:

  -Dorg.apache.deltaspike.ProjectStage=Development

However what if we wanted to use a consistent environment variable across all of our applications (whether they are Java, Ruby, Go or a Shell script) rather than a Java and CDI specific configuration. In this case we will add a custom DeltaSpike configuration source to read our environment variable and configure the CDI container with the appropriate ProjectStage.

/**
 * Implementation of Deltaspike ConfigSource to set the ProjectStage based off an "environment" variable
 */
public class CdiConfigSource implements ConfigSource {

    private final Map<String, ProjectStage> mapping;
    private final Map<String, String> properties;

    public CdiConfigSource() {
        this.mapping = new HashMap<>();
        this.mapping.put("development", ProjectStage.Development);
        this.mapping.put("test", ProjectStage.UnitTest);
        this.mapping.put("staging", ProjectStage.Staging);
        this.mapping.put("production", ProjectStage.Production);

        this.properties = new HashMap<>();
        String env = System.getenv("environment");
        if (env == null || env.trim().length() == 0) {
            env = "test";
        }
        env = this.mapping.get(env).getClass().getSimpleName();
        properties.put("org.apache.deltaspike.ProjectStage", env);
    }

    @Override
    public int getOrdinal() {
        return 500;
    }

    @Override
    public Map<String, String> getProperties() {
        return this.properties;
    }

    @Override
    public String getPropertyValue(String string) {
        return this.properties.get(string);
    }

    @Override
    public String getConfigName() {
        return "cdiConfigSource";
    }

    @Override
    public boolean isScannable() {
        return false;
    }
}

In the above code, we are reading from the environment variable named environment. Based on that field, we are specifying which ProjectStage to use or defaulting to a UnitTest ProjectStage.

The other item to note is that the getOrdinal() method returns a value higher than any other in-built DeltaSpike config sources would return. This means our custom config source will get loaded as a priority.

The last step is to add a file to the META-INF.services directory enabling our config source:

com.tearsofaunicorn.example.CdiConfigSource

With the custom config source in place, deployments to different environments can now be customised at runtime through commands such as:

    environment=development java -jar deltaspike-config-example-1.0.0-SNAPSHOT.jar

In this particular example, the Development ProjectStage would be configured from the environment flag and based on the way the @Exclude annotations were configured, the DirectMailer implementation would be active.