Configuration
SeedStack provides a simple and powerful configuration system. Configuration can be read from multiple sources and in
multiple formats. The recommended way to specify configuration is a YAML file
named application.yaml
located at the root of the classpath.
Sources
Configuration is read from multiple predefined sources, merged into a unique global configuration tree. Other sources can be provided by add-ons.
Base
Base configuration is read from an application.yaml
file at the root of the classpath:
src/main/resources
└ application.yaml
Base configuration is the easiest way of specifying configuration and should be enough for most applicative needs.
If multiple JAR files or classpath directories contain an application.yaml
file, they are all merged together in
an undefined order.
Auto-discovered
Auto-discovered configuration is read from *.yaml
files discovered under the META-INF/configuration
classpath location:
src/main/resources
└ META-INF
└ configuration
├ file1.yaml
├ file2.yaml
└ ...
Auto-discovered configuration is useful to embed configuration in reusable modules as it will be automatically discovered and merged into the global configuration when this module is used. There is no restriction on file names.
Auto-discovered configuration has a lower precedence than base configuration and can be overridden by it.
Command-line
Command-line configuration is gathered from system properties prefixed by seedstack.config
:
java -Dseedstack.config.someConfig.string=someValue -jar app.jar
This will define the following configuration tree:
someConfig:
string: someValue
Command-line configuration has a higher precedence than base configuration and will override it.
Environment variables
Environment variables configuration is gathered from all environment variables and put under the env
node:
env:
ENV_VARIABLE_1: value1
ENV_VARIABLE_2: value2
...
As such, to reference the environment variable ENV_VARIABLE_1
a macro can be used: ${env.ENV_VARIABLE_1}
. Environment
variables have the maximum precedence and cannot be overridden.
System properties
System properties configuration is gathered from all Java system properties and put under the sys
node:
sys:
file.encoding: UTF-8
some.system.property: value1
some.other.system.property: value2
...
As such, to reference the environment variable file.encoding
a macro can be used: ${sys.file\.encoding}
. System properties
have the maximum precedence and cannot be overridden.
Some default system properties (like java.vendor
) have a value and simultaneously serve as prefix for other keys (like
java.vendor.url
). This cannot be mapped as a valid configuration tree, so they are mapped as flat properties under the
sys
node. You must escape the property dots with a backslash (\
) when specifying the path to such keys.
In this example, you can access the file.encoding
property with the sys.file\.encoding
path.
Priority
The priority order of the sources is, from highest to lowest:
- System properties,
- Environment variables,
- Command-line configuration,
- Base configuration,
- Auto-discovered configuration.
Usage
Consider the following Java class:
@Config("someConfig")
public class MyConfig {
@NotBlank
private String string = "defaultValue";
private int[] array;
private InnerConfig object = new InnerConfig();
public static class InnerConfig {
private List<String> list = new ArrayList<>();
}
}
This can be mapped from the following application.yaml
file:
someConfig:
string: Hello World!
array: [1, 2, 3]
object:
list: [iris, jasmine, kiwi]
Declarative API
To inject configuration objects, use the @Configuration
annotation:
public class SomeClass {
@Configuration
private MyConfig myConfig;
}
This will map the global configuration to an instance of the MyConfig
class and inject it into the field. You can
specify the following parameters on the annotation:
value
: this string can be used to specify the path of the configuration node to map. If aCoffig
annotation is present on the mapped class, the path can be omitted.mandatory
: if true, an exception will be thrown when the configuration node is missing; if false the behavior will depend upon the parameter below.injectDefault
: only applies when the configuration node is missing andmandatory
is false. If true a default value will be injected; if false the field will be left as-is allowing to customize the default value.
If you don’t want to define configuration objects, you can still map individual tree nodes:
public class SomeClass {
@Configuration("someConfig.string")
private String someString; // will be "Hello World!"
@Configuration("someConfig.array[1]")
private int someInt; // will be 2
@Configuration(value = "someConfig.object.list[1]", mandatory = true)
private String otherString; // will be "jasmine"
@Configuration(value = "unknownProperty", injectDefault = false)
private String defaultValue = "default"; // will be "default"
}
Programmatic API
To access the configuration programmatically, inject the Application
class and use
the getConfiguration()
method. This will return a Coffig
instance that is the API
facade of the configuration system:
public class SomeClass {
@Inject
private Application application;
public void someMethod() {
Coffig coffig = application.getConfiguration();
// will be the fully mapped POJO
MyConfig myConfig = coffig.get(MyConfig.class, "someConfig");
// will be "Hello World!"
String someString = coffig.get(String.class, "someConfig.string");
// will be 2
int someInt = coffig.get(int.class, "someConfig.array[1]");
// will be "jasmine"
String otherString = coffig.getMandatory(String.class,
"someConfig.object.list[1]");
// will be "default"
String defaultValue = coffig.getOptional(String.class, "unknownProperty")
.orElse("default");
}
}
Class configuration
It is possible to assign configuration properties to classes, individually or by package. The top-level tree node must
be named classes
. Below that, each node having a value will denote a configuration property at its level in the package
hierarchy. Consider this example:
classes:
org:
myorg:
myapp:
appLevelProperty: value1
domain:
packageLevelProperty: value2
model:
SomeModelClass:
classLevelProperty: value3
You can obtain the configuration properties assigned to a class by using the Application
interface:
public class SomeClass {
@Inject
private Application application;
public void someMethod() {
ClassConfiguration<SomeModelClass> classConfig =
application.getConfiguration(SomeModelClass.class);
// will be 'value1'
classConfig.get("appLevelProperty");
// will be 'value2'
classConfig.get("packageLevelProperty");
// will be 'value3'
classConfig.get("classLevelProperty");
}
}
- Class configuration properties are cumulative from the top package to the class level.
- Specific properties (lower-level) override general (higher-level) properties with the same name.
Mapping
The following mapping rules apply:
- Fields are mapped by name from configuration nodes.
- When a setter is present for the field, it is used instead of direct field access.
- When a configuration node is missing for a field, the field is left untouched. This can be used to specify default values.
- If a
@Config
annotation is specified on the mapped class, it is used as the default path prefix.
The following types can be mapped directly from configuration nodes:
- Values (primitive or objects)
- Strings
- Arrays
- Lists
- Sets
- Maps
- Enums
- Optionals
- URIs/URLs
- Properties
- Classes
Types that don’t have a direct mapping (i.e. not in the list above) are mapped by recursively mapping their individual fields. The example at the top of the page is mapped like this:
- The
MyConfig
class cannot be mapped directly, so its fields will be enumerated and mapped individually. - The
string
andarray
fields are mapped directly. - The
InnerConfig
class cannot be mapped directly, so its fields will be enumerated and mapped individually. - The
list
field can be mapped directly. - The items of the list can be mapped directly.
The mapping takes into account the full type of the field, including generics. As such it is possible to properly map complex types like:
List<URL>
,Class<? extends SomeInterface>
,- or even
Optional<Map<String, List<SomeEnum>>>
.
Validation
Bean validation 1.1 annotations can be put on fields mapped by the configuration system.
The constraints will be automatically checked after mapping. If a constraint is violated a ConfigurationException
will be thrown with the details.
Profiles
One or more configuration profiles can be attached to any configuration node with the following syntax:
someConfig:
someProperty<profile1>: value1
someProperty<profile2,profile3>: value2
otherConfig<profile3>:
otherProperty: value3
You can activate one or more configuration profile at application startup by specifying them in the seedstack.profiles
system property:
java -Dseedstack.profiles="profile2,profile3" ...
Considering the example above, if you activate profile1
, the resulting configuration will be:
someConfig:
someProperty: value1
If you activate profile2
and profile3
, the resulting configuration will be:
someConfig:
someProperty: value2
otherConfig:
otherProperty: value3
Macros
Macros are references to other nodes in the global configuration tree and are replaced by the value of the referenced
node. Macros use the ${}
syntax:
name: World
message: Hello ${name}!
Evaluation
In the example above, the node message
will be evaluated to «Hello World!». Note that the macro must specify the full
path of the referenced node.
A macro is evaluated each time the containing node is mapped. This means that if the referenced node has been modified, the node containing the macro will change accordingly.
A macro is evaluated recursively so it can reference a node containing another macro and so on.
Escaping
Macro resolution can be avoided by escaping the dollar sign with a backslash: Hello \${name}!
will be evaluated to «Hello ${name}!».
Nesting
Macros can be nested:
names: [ John, Jane ]
index: 1
message: Hello ${names[${index}]}!
The message
node will be evaluated to «Hello Jane!».
Default value
If a macro references a non-existing node, it will be evaluated to an empty string. A macro can have a default value that can be a quoted string or another node reference.
name: World
message1: Hello ${name}!
message2: Hello ${foobar}!
message3: Hello ${foobar:'Robert'}!
message4: Hello ${foobar:name}!
- The
message1
node will be evaluated to «Hello World!». - The
message2
node will be evaluated to «Hello !». - The
message3
node will be evaluated to «Hello Robert!». - The
message4
node will be evaluated to «Hello World!».
Function calls
Function calls allow to call predefined Java methods from configuration nodes. Function calls use the $fn()
syntax:
symbols: [ '!', '.', '?' ]
message: $greet('World', symbols, 0)
The greet
function is implemented as a method annotated with @ConfigFunction
in a class implementing ConfigFunctionHolder
:
package org.seedstack.samples.config;
public class GreetFunctionHolder implements ConfigFunctionHolder {
private Coffig coffig;
@Override
public void initialize(Coffig coffig) {
this.coffig = coffig;
}
@ConfigFunction
private String greet(String name, String[] symbols, int index) {
return String.format("Hello %s %s", name, symbols[index]);
}
}
The GreetFunctionHolder
class must be registered with the Java ServiceLoader mechanism,
with the following file:
src/main/resources
└ META-INF
└ services
└ org.seedstack.coffig.spi.ConfigFunctionHolder
containing:
org.seedstack.samples.config.GreetFunctionHolder
The holder is not injectable because it is possible for configuration functions to be called before the injector is ready. You must implement your functions in a way that doesn’t rely on injection.
Evaluation
In the example above, the node message
will be evaluated to «Hello World!». Parameters are automatically mapped to their
Java type, allowing to pass complex objects to configuration functions. String literals can be passed as parameters by
enclosing them with single quotes. Parameters that reference other nodes must specify their full path.
Escaping
Function calls can be avoided by escaping the dollar sign with a backslash: \$greet('World', symbols, 0)
will be
evaluated to «$greet(‘World’, symbols, 0)».
Nesting
Parameters can be other functions calls: $greet('World', $getSymbols(), 0)
will use the return value of the getSymbols()
function call as second parameter.
Override
Any configuration file can be overridden by adding the «override» word in its name before the extension:
- Any node inside an
application.override.yaml
file can override any node inside anapplication.yaml
file, - Any node inside a
META-INF/configuration/*.override.yaml
can override any node inside aMETA-INF/configuration/*.yaml
file.
You can use this feature to override configuration in tests. The easiest way to do this is to add an application.override.yaml
at the root of your test classpath.
Formats
YAML
Any configuration file can be in the YAML format. The files can use the yaml
or
the yml
extension indifferently.
This is the preferred configuration format.
JSON
Any configuration file can also be in JSON format, by using the json
extension
instead. The same rules apply to YAML and JSON files.
Properties
Any configuration file can also be in Properties format,
by using the properties
extension instead.
Dot-delimited properties are mapped hierarchically. For instance the following properties file:
test.key1=value1
test.key2=value2
Is equivalent to the following YAML file:
test:
key1: value1
key2: value2
One important side-effect is that you cannot give a value to a property already used as a prefix of another property.
In this example, the test
property cannot be given any value since it cannot be both parent and leaf at the same time
in the resulting tree.
Properties files only support string values. Other data types, such as numbers, booleans, arrays or lists are not supported and must be parsed manually.
Tooling
For more information about the tool mode, see this page.
Configuration options dump
To dump the all the configuration options available in your project you can execute the config goal of the SeedStack Maven plugin:
mvn -q seedstack:config
This executes the config
tool on your project and dump a tree of all configuration options with their type and
description. The -q
disables all non-error Maven logs to clean the output. You can ask for details about a specific leaf
in the configuration tree:
mvn -q -Dargs="application.basePackages" seedstack:config
This displays the detailed description of the application.basePackages
configuration node.
Effective configuration
To dump the configuration effectively scanned, parsed and aggregated by SeedStack you can execute the effective-config goal of the SeedStack Maven plugin:
mvn -q seedstack:effective-config
This executes the effective-config
tool on your project and produce a YAML dump of the aggregated configuration as seen
by the application. The -q
disables all non-error Maven logs to clean the output.
You can execute the plugin effective-test-config goal to display the test configuration instead of the main one:
mvn -q seedstack:effective-test-config
This takes into account the test classpath to build the configuration tree.