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 property files.

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 initialize and load configuration when it is first used.

initialization

 

config.load.systemProperties

If we set config.load.systemProperties to true, all loaded properties are then set into the 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 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 five 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 a application.yaml or application.properties file.

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

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 two external files are configuration files that are loaded providing configuration values for the application.

3. avaje.profiles

Setting a avaje.profiles property or AVAJE_PROFILES environment variable will cause avaje config to load the property files in the form application-<profile>.properties (will also work for yml/yaml files). For example, if you set the avaje.profiles property (perhaps via command line) to dev,docker it will attempt to load application-dev.properties and application-docker.properties.

4. load.properties

We can specify a load.properties property to define additional 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.

5. ConfigurationSource

We can also use plugins that implement ConfigurationSource to load configuration from other sources. Refer to the ConfigurationSource 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 will be 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 initializers, enums, constructors etc. There is no limitation on where we can use Config.

Config has convenient static methods to access the underlying configuration. 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 a List/Set of String, int, long.

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

Set<String> operations = Config.set().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));

List<CustomObj> operations = Config.list().ofType("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 provides a static singleton scope for the underlying Configuration. Generally there should not be much need to access the underlying configuration, as the static access is much more convenient.

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 are 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 the configuration after all files have loaded, and then use setProperty(key, value) to add/modify properties. The values can contain expressions and will have their expressions evaluated as part of setProperty.

Refer to the example plugin - MyExternalLoader.java

Event Logging

By default, avaje-config will immediately log initialization 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 initialized.
 */
default void preInitialisation() {
}

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