Loading...

November 3, 2015

Ratpacked: Externalized Application Configuration

Ratpack has very useful methods to apply application configuration in our application. We can read configuration properties from files in different formats, like JSON, YAML and Java properties, and the files can be read from different locations, like class path or file system. We can also set configuration properties via Java system properties on the command line or use environment variables.

We use the ratpack.config.ConfigData class with the static of method to add configuration properties to our application. We provide a lambda expression or Groovy closure to the of method to build our configuration. Here we specify external files, locations and other configuration options we want to include for our application. If the same configuration property is defined in multiple configuration sources Ratpack will apply the value that is last discovered. This way we can for example provide default values and allow them to be overridden with environment variables if we apply the environment variables last.

To use the values that are gathered we use the get method of the ConfigData instance. We can apply the configuration properties to the properties of a configuration class that is then automatically instantiated. We add this to the registry so we can use the configuration properties further down in our application.

In the following example Ratpack application we use different ways to apply configuration properties. It is inspired by how Spring Boot reads and applies externalized configuration properties. First we use a simple Map with default values, then the class path is scanned for files with names application.yml, application.json and application.properties in either the root of the class path or a config package. Next the same file names are searched on the file system relative from where the application is started. Next Java system properties starting with sample. are applied to the configuration. And finally environment variables starting with SAMPLE_ are interpreted as configuration properties.

Let's start with the simple configuration class and properties we want to set via Ratpack's configuration ability:

// File: src/main/groovy/com/mrhaki/SampleConfig.groovy
package com.mrhaki

/**
 * Configuration properties for our application.
 */
class SampleConfig {

    /**
     * URL for external service to invoke with HTTP client.
     */
    String externalServiceUrl

    /**
     * URI to access the Mongo database.
     */
    String mongoUri

    /**
     * Indicate if we need to use a HTTP proxy.
     */
    boolean useProxy

    /**
     * Simple message
     */
    String message

}

Next we have a very simple Ratpack application. Here we use ConfigData.of with a lot of helper methods to read in configuration properties from different sources:

// File: src/ratpack/Ratpack.groovy
import com.google.common.io.Resources
import com.mrhaki.SampleConfig
import ratpack.config.ConfigData
import ratpack.config.ConfigDataBuilder

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

import static groovy.json.JsonOutput.prettyPrint
import static groovy.json.JsonOutput.toJson
import static ratpack.groovy.Groovy.ratpack

ratpack {

    bindings {
        final ConfigData configData = ConfigData.of { builder ->
            // Set default value, can be overridden by
            // configuration further down the chain.
            // The map must have String values.
            builder.props(['app.useProxy': Boolean.TRUE.toString()])

            loadExternalConfiguration(builder)

            // Look for system properties starting with
            // sample. to set or override configuration properties.
            builder.sysProps('sample.')

            // Look for environment variables starting
            // with SAMPLE_ to set or override configuration properties.
            builder.env('SAMPLE_')

            builder.build()
        }

        // Assign all configuration properties from the /app node
        // to the properties in the SampleConfig class.
        bindInstance(SampleConfig, configData.get('/app', SampleConfig))
    }

    handlers {
        get('configprops') { SampleConfig config ->
            render(prettyPrint(toJson(config)))
        }
    }

}

private void loadExternalConfiguration(final ConfigDataBuilder configDataBuilder) {
    
    final List<String> configurationLocations =
            ['application.yml',
             'application.json',
             'application.properties',
             'config/application.yml',
             'config/application.json',
             'config/application.properties']
    
    configurationLocations.each { configurationLocation ->
        loadClasspathConfiguration(configDataBuilder, configurationLocation)
    }
    
    configurationLocations.each { configurationLocation ->
        loadFileSystemConfiguration(configDataBuilder, configurationLocation)
    }
}

private void loadClasspathConfiguration(
        final ConfigDataBuilder configDataBuilder,
        final String configurationName) {

    try {
        final URL configurationResource = Resources.getResource(configurationName)
        switch (configurationName) {
            case yaml():
                configDataBuilder.yaml(configurationResource)
                break
            case json():
                configDataBuilder.json(configurationResource)
                break
            case properties():
                configDataBuilder.props(configurationResource)
                break
            default:
                break
        }
    } catch (IllegalArgumentException ignore) {
        // Configuration not found.
    }

}

private void loadFileSystemConfiguration(
        final ConfigDataBuilder configDataBuilder,
        final String configurationFilename) {

    final Path configurationPath = Paths.get(configurationFilename)
    if (Files.exists(configurationPath)) {
        switch (configurationFilename) {
            case yaml():
                configDataBuilder.yaml(configurationPath)
                break
            case json():
                configDataBuilder.json(configurationPath)
                break
            case properties():
                configDataBuilder.props(configurationPath)
                break
            default:
                break
        }
    }
}

private def yaml() {
    return hasExtension('yml')
}

private def json() {
    return hasExtension('json')
}

private def properties() {
    return hasExtension('properties')
}

private def hasExtension(final String extension) {
    return { filename -> filename ==~ /.*\.${extension}$/ }
}

Next we create some external configuration files:

# File: src/ratpack/application.yml
---
app:
  mongoUri: mongodb://mongo:27017/test
# File: src/ratpack/application.properties
app.externalServiceUrl = http://remote:9000/api
app.message = Ratpack rules!

Let's run the application and see the output of the configprops endpoint:

$ http localhost:5050/configprops
...
{
    "externalServiceUrl": "http://remote:9000/api",
    "useProxy": true,
    "message": "Ratpack rules!",
    "mongoUri": "mongodb://mongo:27017/test"
}

Next we stop the application and start it with the Java system property -Dsample.app.useProxy=false and the environment variable SAMPLE_APP__MESSAGE='Ratpack rocks!'. We check the configprops endpoint again:

$ http localhost:5050/configprops
...
{
    "externalServiceUrl": "http://remote:9000/api",
    "useProxy": false,
    "message": "Ratpack rocks!",
    "mongoUri": "mongodb://mongo:27017/test"
}

Written with Ratpack 1.0.0.

This is the 1000th blog post on the Messages from mrhaki blog. I've blogged about many different subjects, starting with Apache Cocoon, Netbeans, followed by Groovy and Groovy related technologies like Grails, Gradle. Also about other developer subjects like Asciidoctor and much more. I hope you enjoyed the first 1000, because I am not finished and will continue with more blog posts about great technologies.