Avaje Config

avaje-config provides external configuration for JVM apps. We can provide configuration via yaml or properties files and specify which files to load using command line argument and resources.

License Source API Docs Issues Releases
Apache2 Github Javadoc Github

 

Dependency

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-config</artifactId>
  <version>${config-version}</version>
</dependency>

 

Expression evaluation

Expressions start with ${ end with }. They can optionally define a default value using a : as we see in the example below for the username and password which have default values of mydb and notSecure respectively.

Expressions are evaluated using environment variables, system properties as well as other properties.

example
app.name: myapp
images.home: ${user.home}/myapp/images

datasource:
  db:
    username: ${DB_USER:mydb}
    password: ${DB_PASS:notSecure}
    url: ${DB_URL}

Startup

avaje-config will initialise and load configuration when it is first used.

Initialisation

 

config.load.systemProperties

If we set config.load.systemProperties to true then all the properties that have been loaded are then set into system properties. You can provide an additional property system.excluded.properties to provide a list of configuration properties you want to exclude from being loaded into the System Properties.

Loading configuration

avaje-config provides ways to load external configuration for running an application and running tests. It also provides a simple mechanism to make it easy to run the application locally (typically in order to run integration tests locally).

Running app

There are four main ways to provide configuration used for the purpose of running the application.

1. Default values via main resources

We can provide "default" configuration values by putting into src/main/resources either application.yaml or application.properties.

These values act as default values and can be overwritten by configuration provided by other sources such as files specified by command line arguments.

2. Command line arguments

We can specify external configuration files to use via command line arguments. Arguments proceeded by -P are considered possible configuration files and avaje-config will try to load them. If they are not valid files that exist then they are ignored.

example
java -jar myapp.jar -P/etc/config/myapp.properties -P/etc/other.yaml

In the example above 2 external files are configuration files that are loaded providing configuration values for the application.

3. load.properties

Optionally we specify a load.properties property to define configuration files to load.

example in application.yaml
## we don't need to specify path if it's in src/main/resources
## can be combined with eval to have something like feature flags
load.properties: ${ENV:local}.properties /etc/other.yaml

After default configuration files are loaded the load.properties property is read and if specified these configuration files are loaded.

load.properties is pretty versatile and can even be chained. For example, in using the above properties, you can load based on the ENV environment variable/-Dprofile JVM arg, and in the loaded property file you can add load.properties to load even more properties and so on.

4. Plugins

We can also use plugins that implement ConfigurationSource to load configuration from other sources. Refer to the Plugins section for more details.

Running Tests

To provide configuration for running tests add into src/test/resources either application-test.yaml or application-test.properties.

The configuration from these files is loaded and used when running tests. This configuration will override any configuration supplied by files in src/main/resources.

Running locally

When we run our application locally we can provide configuration for this via properties or yaml files in a ~/.localdev directory.

For example, when running locally we want to provide configuration that configures our application to use a local database. We want to do this when we run integration tests locally running application(s).

app.name

We must specify a app.name property to do this.

example
app.name=myapp

Then in ~/.localdev put a yaml or properties configuration file using the app.name. For example, given app.name=myapp have either ~/.localdev/myapp.yaml or ~/.localdev/myapp.properties define the configuration we want to use when running locally.

File watching

If config.watch.enabled is set to true then avaje-config will watch the config files and reload configuration when the files change.

By default it will check the config files every 10 seconds. We can change this by setting config.watch.period

example

config:
  watch:
    enabled: true
    period: 5   ## check every 5 seconds

We can use this as a simple form of feature toggling. For example if we are using kubernetes and have configuration file based on a config map, we can edit the config map to enable/disable features.

config.watch.enabled: true

## some config values to toggle features on/off
feature.cleanup.enabled: true
feature.processEmails.enabled: false
if (Config.enabled("feature.cleanup.enabled") {
  ...
}

Config

We use Config to access the application configuration. Config can be used anywhere in application code - static initialisers, constructors etc. There is no limitation on where we can use Config.

Config has convenient static methods to access the underlying configuration and this is how the majority of applications will use it. It also has asConfiguration() to return the underlying configuration and asProperties() to return the loaded configuration as standard properties.

Get Property

Config provides method to get property values as String, int, long, boolean, BigDecimal, Enum, URI and Duration.

Examples
// get a String property
String value = Config.get("myapp.foo");

// with a default value
String value = Config.get("myapp.foo", "withDefaultValue");
int port = Config.getInt("app.port");
long val = Config.getInt("foo", 42);
long port = Config.getLong("app.port");
long val = Config.getLong("foo", 42);
boolean val = Config.getBool("feature.cleanup");
boolean val = Config.getBool("feature.cleanup", true);

// Config.enabled() is an alias for Config.getBool()
boolean val = Config.enabled("feature.cleanup");
boolean val = Config.enabled("feature.cleanup", true);
BigDecimal val = Config.getDecimal("myapp.tax.rate");
BigDecimal val = Config.getDecimal("myapp.tax.rate", "12.5");
MyEnum val = Config.getEnum(MyEnum.class, "myapp.code");
MyEnum val = Config.getEnum(MyEnum.class, "myapp.code", MyEnum.DEFAULT);
URI val = Config.getURI("other.url");
URI val = Config.getURI("other.url", "https://other:8090");
List and Set

We can also return configuration values as List or Set of String, int and long.

List<Integer> codes = Config.getList().ofInt("my.codes", 42, 54);

Set<String> operations = Config.getSet().of("my.operations", "put", "delete");
Function

We can also provide a function to map a config to a type.

CustomObj codes = Config.getAs("my.codes", s -> new CustomObj(s));

Optional<CustomObj> operations = Config.getAsOptional("my.codes", s -> new CustomObj(s));

Set Property

Use setProperty() to set configuration values.

When we set values this will fire any configuration callback listeners that are registered for the key.

Config.setProperty("other.url", "https://bazz");

Config.setProperty("feature.cleanup", "false");

Event Publishing

Use eventBuilder() to publish multiple changes at once.

Config.eventBuilder("EventName")
   .put("someKey", "val0")
   .put("someOther.key", "42")
   .remove("foo")
   .publish();

On Change

We can register callbacks that execute when a configuration key has it's value changed.

Single property onChange
Config.onChange("other.url", newUrl -> {
  // do something interesting with the changed
  // config value
  ...
});
Multi property onChange
Config.onChange(event -> {

      Set<String> changedKeys = event.modifiedKeys();

      Configuration updatedConfig = event.configuration();

      // do something with the changed values.
    });

// Filter events for specific properties
Config.onChange(event -> {

      Set<String> changedKeys = event.modifiedKeys();

      Configuration updatedConfig = event.configuration();

      // do something with the changed values.
    }, "someKey", "someOther.key");

asProperties()

Obtain all the configuration values as standard properties using Config.asProperties()

Properties properties = Config.asProperties();

asConfiguration()

Obtain the underlying configuration via Config.asConfiguration(). Config is providing static singleton scope for the underlying Configuration. We generally always use Config because it is convenient and there should not be much need to access the underlying configuration.

Configuration configuration = Config.asConfiguration();

forPath()

Obtain a filtered configuration via Config.forpath().

//given properties like the below in application.properties
database.example.password=secretPassword
database.example.username=notSecretUsername

Configuration dbConfiguration = Config.forPath("database");
Configuration dbExampleConfiguration = dbConfiguration.forPath("example");
// or more simply
Configuration dbExampleConfiguration = Config.forPath("database.example");
// will return secretPassword
dbExampleConfiguration.get("password");
// will return secretPassword
dbExampleConfiguration.get("username");

ConfigurationSource Plugins

Plugins implement the ConfigurationSource interface and found and registered via ServiceLoader. This means they have a file at src/main/resources/META-INF/services/io.avaje.config.ConfigurationSource which contains the class name of the implementation.

Plugins implement the method

void load(Configuration configuration);

They are able to use configuration to read properties if needed, for example read the host name of a redis server to read configuration from.

Plugins typically read their configuration source and then use setProperty(key, value) to set the properties into configuration. The values can contain expressions and will have their expressions evaluated as part of setProperty.

Refer to the (silly) example plugin - MyExternalLoader.java

Event Logging

By default, avaje-config will immediately log initialisation events to it's own configured system logger. If you want to use your own configured logger, you can extend the ConfigurationLog interface and register via ServiceLoader. This means you have a file at src/main/resources/META-INF/services/io.avaje.config.ConfigurationLog which contains the class name of the implementation.

Custom Event loggers implement the methods

/**
 * Log an event with the given level, message, and thrown exception.
 */
void log(Level level, String message, Throwable thrown);

/**
 * Log an event with the given level, formatted message, and arguments.
 * <p>
 * The message format is as per {@link java.text.MessageFormat#format(String, Object...)}.
 */
void log(Level level, String message, Object... args);

Additionally, you can implement the two default methods and provide behavior before and after the configs are loaded.

/**
 * Invoked when the configuration is being initialised.
 */
default void preInitialisation() {
}

/**
 * Invoked when the initialisation of configuration has been completed.
 */
default void postInitialisation() {
}