ConfiJ Logo

ConfiJ is a type-safe, strongly defined, minimal-dependency Java configuration library. It enforces a contract on the configuration as Java-types.

Getting Started

ConfiJ requires Java 1.8 or later. It is modularly organized in order to reduce the amount of dependencies. confij-core itself has no external dependencies.

1. Repository

ConfiJ maven artifacts are available on Maven Central.

Example 1. Sample Gradle setup
repositories {
   mavenCentral()
}
dependencies {
    implementation 'ch.kk7:confij-core:0.6.6:nodeps'
}

Artifacts with two different classifiers are published. The nodeps ones do not depend on any non-shadowed 3rd party libs and are generally prefered. The default ch.kk7:confij-core:0.6.6 might introduce dependency issues only if an incompatible lib is already on the classpath (for example an old com.fasterxml:classmate).

2. Quick Start

Example 2. Getting started with interface configuration and ConfiJ-Builder
interface ServerConfig { (1)
	String name();
	URL externalUrl();
}
ServerConfig serverConfig = ConfijBuilder.of(ServerConfig.class)
		.loadFrom("server.properties") (2)
		.build(); (3)
# content of a file named server.properties
name=deepthough
externalUrl=https://example.com:8042/think
1 Define your configuration type, in this case as a Java-interface
2 Load from various sources, in this case from a single properties file
3 Bind the source to a configuration instance of matching type

The corresponding properties file must have the same attributes as in the interface. ConfiJ will make sure no unknown keys are present and bind all configuration strings to their configuration types.

Configuration Sources

Sources are where configuration values come from. This can be anything from a file to git to envvars in various formats. Confij will aways fail fast upon encountering an unknown value.

3. Source Pipelines

ConfiJ loads sources one after the other. Each consecutive source overrides (merges) attributes of the previous ones. The last source therefore has the highest preference. Typically, this is useful to load a set of default values first, then continue with application specifics and environment specific values getting more and more detailed. Source types can be freely mixed, meaning you can load from classpath, then from a remote file, then from envionment variables and so on.

Example 3. Merge multiple configuration files across all formats
interface ServerConfig {
	String name();

	URL externalUrl();

	@Default("1")
	int line();

	@Default("30s") (1)
	Duration timeout();
}
ServerConfig serverConfig = ConfijBuilder.of(ServerConfig.class)
		.loadFrom("classpath:generic.properties") (2)
		.loadFrom("server.properties") (3)
		.loadFrom("sys:app") (4)
		.build();
1 Default values are always loaded first.
2 then override with a file from the classpath root.
# application.properties
name=appserver
3 then override with a file on disk (this is the first time where name is defined)
# content of a file named server.properties
name=deepthough
externalUrl=https://example.com:8042/think
4 then override with system properties prefixed by app
-Dapp.line=3

This will result in the final configuration:

ServerConfig {
    externalUrl=https://example.com:8042/think,
    line=3,
    name=deepthough,
    timeout=PT30S
}

Validation generally only applies to the final configuration. It is allowed to have incomplete configurations while there are still more configuration sources to load. However, it is never allowed to have an unknown configuration value. Confij has a default fail-fast strategy such that you don’t have to worry about legacy configs.

4. Default Values

Configuration default can be defined directly in your configuration interface using the @Default annotation. For more complex scenarios where the default value is not easily stingifyable, use a default method.

Example 4. Default configuration values as annotations
interface HasDefaults {
	@Default("a default value")
	String aString();

	@Default("23")
	int aNumber();

	default List<Boolean> aListOfBooleans() {
		return Arrays.asList(true, false);
	}

	default int aNumberPlus1() {
		return aNumber() + 1;
	}
}

The default method is called only when no other configuration values is defined. However only @Default values can be used for templating.

5. The AnySource

The AnySource simply takes a string and determines resource location and type based on it. It is the most compact form to specify a source unless you require special settings.

Example 5. Load from multiple sources of dynamic type and location
ConfijBuilder.of(Config.class)
		.loadFrom("nested.properties") // properties file in current working directory
		.loadFrom(new File("nested.properties").getAbsolutePath()) // equivalent
		.loadFrom("file:nested.properties") // equivalent
		.loadFrom("classpath:nested${key}.yaml") // a YAML file on the classpath root
		.loadFrom("${key}:nestedvalue.yaml") // ...with variable replacements
		.loadFrom("sys:some.prefix") // from system properties
		.loadFrom("env:some_prefix") // from environment variables
		.build();

Variable substitutions are supported anywhere in the template string (see Templating). They take into account the previously loaded configuration keys. Replacement keys are always absolute (relative to the configuration root).

Most commonly the resource location (file, classpath,…​) is detected based on a URI scheme (see Section 6.1, “Resource Providers”). The configuration format (yaml, json,…​) is usually detected based on the ending.

6. Source as Code

For more complicated scenarios where Chapter 5, The AnySource is not suited, a ConfijSource must be passed explicitly. Confij distinguishes between where something is read from (Resource) and what format it is.

Example 6. Explicitly written, but same sample as "Example 3, “Merge multiple configuration files across all formats”"
ServerConfig serverConfig = ConfijBuilder.of(ServerConfig.class)
		.loadFrom(ClasspathResource.ofName("generic.properties"))
		.loadFrom(FileResource.ofFile("server.properties"), PropertiesFormat.withoutPrefix())
		.loadFrom(SystemPropertiesSource.withPrefix("app"))
		.build();

6.1. Resource Providers

Configuration sources that can load from a file can usually load from other input streams as well. For those, the source location and source format can be combined freely.

Chapter 5, The AnySource usually detects the location based on the scheme:

  • file: - is the default scheme and loads from the local Filesystem

  • classpath: - load from classpath on the default classloader

  • http: or any other registered URL format

  • git: for GIT repositories, see Section 6.2, “GIT Resource Provider”

Register custom resource providers using a Chapter 7, ConfiJ’s ServiceLoaders for ch.kk7.confij.source.any.ConfijAnyResource.

6.2. GIT Resource Provider

GIT resources require the ch.kk7:confij-git maven artifact.

Configuration sources can be fetched via GIT. For simple cases an Chapter 5, The AnySource can be used with a special URL format:

  • a fixed scheme 'git:'

  • followed by the URI-like path to the repository.

  • followed by the path to the file in the repository. If the repository doesn’t end with .git, separate the filepath with two slashes //.

  • optionally add a refspec pointing to a commit as fragment (separated from the rest by #). The default refspec is HEAD.

Example 7. Valid AnySource-URLs specifying the path to a file in a GIT-repository
# annonymous https GIT repository containing /some/file.yaml
git:https://example.com/repo.git/some/file.yaml

# double slash is required if not ending with .git
git:https://example.com/repo//some/file.yaml

# BasicAuth
git:https://user:password@example.com/repo//some/file.yaml

# annonymous ssh
git:ssh://example.com/annonymous.git/some/file.yaml

# annonymous native git protocol: twice 'git' (sic)
git:git://example.com/annonymous.git/some/file.yaml

# refspec of a commit as SHA1-prefix
git:git://example.com/repo.git/file.yaml#4a4255c

# previous commit of default HEAD (usually the master branch)
git:git://example.com/repo.git/file.yaml#HEAD~1

# file on a GIT-branch named 'fuu'
git:git://example.com/repo.git/file.yaml#refs/heads/fuu

# file tagged with 'v1.0.42'
git:git://example.com/repo.git/file.yaml#refs/tags/v1.0.42

For more complex scenarios like custom SSL authentication consider extending the existing GitResource.

6.3. YAML Format

YAML format requires the ch.kk7:confij-yaml maven artifact.

YAML is a recommended configuration source format since it maps nicely on a tree-structure. Collection types can be represented as lists or as maps with numerical keys.

YAML can be loaded from all the Section 6.1, “Resource Providers”. The AnySource expects a case-insensitive file ending of .yaml or .yml.

Example 8. Load from multiple sources of dynamic type and location
interface ComplexYaml {
	List<String> listOfStrings();
	Map<String, Integer> mapOfIntegers();
	Map<String, Integer> mapOfIntegersClone();
	LinkedList<String> anotherList();
	OffsetDateTime date();
	@Key("true") boolean isTrue();
	@Key("false") boolean isFalse();
}
# complex.yaml
listOfStrings:
  - String
  - String on
    a single line
  - "Double quotation marks\t"
mapOfIntegers: &ref
  canonical:   12345
  decimal:     +12_345
  sexagesimal: 3:25:45
  octal:       030071
  hexadecimal: 0x3039
mapOfIntegersClone: *ref
anotherList:
  0: null
  1: big
date: 2001-12-14t21:59:43.10-05:00
true: true
false: off

If the YAML source file contain multi-docs, they are loaded in order. Internally all values are reduced to Maps of Maps or Strings.

6.4. HOCON/JSON Format

HOCON format requires the ch.kk7:confij-hocon maven artifact.

HOCON (Human-Optimized Config Object Notation) is a format for human-readable data, and a superset of JSON and .properties.

HOCON can be loaded from all the Section 6.1, “Resource Providers”. The AnySourceImpl expects a case-insensitive file ending of .hocon, .json or .conf.

The default behaviour will first perform substitutions according to the HOCON Specification, before ConfiJ’s templating will kick in.

Example 9. Two substitution mechanisms
// HOCON will resolve this ${a} internally
a: 1
b: ${a}" and two"
// HOCON ignores ${a} but ConfiJ will substitute it
a: 1
b: "${a} and two"

6.5. Properties Format

Java properties files are part of the ch.kk7:confij-core due to not having any 3rd party dependencies, but are otherwise not recommended, since it is a flat key-value format.

Property files can be loaded from all Section 6.1, “Resource Providers”. The AnySourceImpl expects a case-insensitive file ending of .properties.

Example 10. Nested configuration loading from properties file
interface Config {
	String key();
	Nested nest(); (1)
	List<Nested> listOfNest(); (2)
	Map<String, Nested> mapOfNest(); (3)
}

interface Nested {
	int x();
	int y();
}
# nested.properties
key=value
(1)
nest.x=0
nest.y=1
(2)
listOfNest.0.x=10
listOfNest.0.y=11
listOfNest.1.y=20
listOfNest.2.y=21
(3)
mapOfNest.mykey.x=100
mapOfNest.mykey.y=101
mapOfNest.myotherkey.x=200
1 Per default, nested configuration keys are separated with a dot from each other.
2 Collection types simply require numerical keys (starting at 0).
3 Map types can use any key excluding the delimiter.

6.6. Environment Variables and System Properties Format

Environment variables and system properties work similar to the file based Section 6.5, “Properties Format”. System properties have to be dot separated, whereas envvars are underline separated. To read an envvar specify a URI like env:<prefix>, where every envvar to be considered must start with that prefix and will have that prefix stripped away. For example an envvar FUU_bar_xy with a prefix FUU is mapped to a property called bar.xy (case-sensitive).

Example 11. Read configuration values from envvars and Java sys props
interface Configs {
	String fromEnvvar();

	String fromSysprop();

	@Default("${env:HOME}")
	Path myHome();

	@Default("${sys:file.separator}")
	String fileSep();
}
> export A_PREFIX_fromEnvvar='some value'
> java -Danother.prefix.fromSysprop="another value" ...

An often used alternative is to rely on Chapter 10, built-in Templating to read specific envvars.

7. ConfiJ’s ServiceLoaders

ConfiJ allows utilizing Java’s ServiceLoader mechanism to register additional services like source formats or validators. All you have to do is to add the file with the service-interface name to META-INF/services. In this service file you list the fully qualified classnames of your constraint validator classes (one per line).

Example 12. Custom Resource Provider as ServiceLoader
// +file: META-INF/services/ch.kk7.confij.source.file.resource.ConfijAnyResource
public static class FooResource implements ConfijAnyResource {
	@Override
	public Optional<ConfijResource> maybeHandle(String path) {
		return Optional.of((ConfijResource) resolver -> Stream.of(new ResourceContent("foo=bar", path)))
				.filter(__ -> path.startsWith("foo:"));
	}
}

interface Foo {
	String foo();
}

	Foo foo = ConfijBuilder.of(Foo.class)
			.loadFrom("foo:fuuuuu.properties")
			.build();

To change the priority of a service loader instance implement the ServiceLoaderPriority.

Example 13. Custom Resource Provider as ServiceLoader with lowered priority
public static class AnUnimportantFoo extends FooResource implements ServiceLoaderPriority {
	@Override
	public int getPriority() {
		return ServiceLoaderPriority.DEFAULT_PRIORITY - 1000;
	}

	@Override
	public Optional<ConfijResource> maybeHandle(String path) {
		return Optional.of((ConfijResource) resolver -> Stream.of(new ResourceContent("foo=OTHER", path)))
				.filter(__ -> path.startsWith("foo:"));
	}
}

Binding

The binding defines the contract of the configuration as well as the mapping from string value to the actual configuration type. Each configuration is organized in a tree-structure and generally immutable.

8. Value Mapping

When values are read from a configuration files they are first available as Strings. They are then transparently mapped to Objects or pimitives to match the defined type.

Example 14. Value Mapping to known types in a flat configuration
interface MappedConfig {
	String aString();

	Double pi(); (1)

	@Key("int") (2)
	int anInt();

	int[] intArray(); (3)

	@Base64 (4)
	byte[] base64();

	Duration duration(); (5)
}
# mapped.yaml
aString: a value
pi: 3.141
int: 42
intArray: [1, 2, 3]
base64: AQIDBA==
duration: 7nanoseconds
1 mapping to primitives or java.lang objects
2 configuration keys can be changed if the attribute name doesn’t fit
3 arrays are usually mapped from lists (see Chapter 9, Nested Configuration)
4 or the default behavior can be changed with a custom ValueMapping.

8.1. Default Value Mapping

The default mapping behavior attempts to map from string to target type in the following order:

  1. if the attribute is annotated with a custom @ValueMapper enforce that one (see Section 8.3, “Custom Value Mapping”)

  2. primitives types, where null values are mapped to their empty types: a non-existent int becomes 0. booleans either have to be lowercase true or false, except for source languages enforcing another definition (like yaml).

  3. explicit mappings, for example:

    java.nio.file.Path
    java.io.File
    java.time.Duration
    java.time.Period
    java.util.Optional
    ...
  4. all Enum-types based on the Enum’s name

  5. classes with a static method named valueOf(String) or fromString(String)

  6. classes with a constructor taking a single String as input argument

Arrays/Collections are not considered leafs. In order to map a comma separated configuration string to a string[] you have to annotate it with @Separated.

8.2. Built-in Value Mapping

8.2.1. Base64 encoded

@Base64 expects to bind a base64-encoded string to an array or collection of bytes.

Example 15. Mapping base64 encoded configuration properties
interface Base64Encoded {
	@Base64
	byte[] base64Arr();

	@Base64(decoder = Base64Decoder.RFC4648_URLSAFE)
	List<Byte> base64List();
}

8.2.2. Comma Separated

@Separated expects to bind a delimited string to an array or collection of arbitrary type.

Example 16. @Separated as a short-form for collection types
interface SeparatedConfig {
	@Separated
	List<String> commaSeparated();

	@Separated(separator = "#")
	int[] hashSeparated();

	Set<String> usuallyAList();
}
commaSeparated=comma,separated,values
hashSeparated=1#2#3
usuallyAList.0=common
usuallyAList.1=list
usuallyAList.2=notation

8.3. Custom Value Mapping

If the default mapping does not fit or the target type is unknown, you generally have two options: programmatic the ConfiJBuilder or with an annotation on the affected attribute.

If a custom mapping holds for all types in the whole configuration, it’s easiest to set it in the builder when creating the configuration instance.

Example 17. Global support to map RGB codes like "#0F0F0F" to Color
EmptyColorHolder colorHolder = ConfijBuilder.of(EmptyColorHolder.class)
		.bindValuesForClassWith(Color::decode, java.awt.Color.class)

If a custom mapping should only be applied to specific attributes, annotate them with @ValueMapper(Class). The mapper class must have an accessible empty constructor.

Example 18. Annotation based support to map RGB codes like "#0F0F0F" to Color
static class ColorDecoder implements ValueMapperFactory {
	@Override
	public Optional<ValueMapperInstance<?>> maybeForType(BindingType bindingType) {
		return Optional.of(Color::decode);
	}
}

interface Favourites {
	@ValueMapper(ColorDecoder.class)
	Color favouriteColor();
}

In order to pass compile time options to the mapper instance, introduce fresh custom annotation, annotated with @ValueMapper(Class). See for example the implementation of Section 8.2.2, “Comma Separated”.

9. Nested Configuration

Configurations can themselves contain child configurations recursively. Since every configuration is organized in a tree-structure, we call them branches.

Example 19. Simple nested configuration
interface ServerConfig {
	Timing timing();
	URL url();
}

interface Timing {
	Duration keepAlive();
	Duration timeout();
}

Typical branches contain, but are not limited to:

  • nested interfaces (also with generics)

  • Collection of interfaces, Objects (leafs), other Collections,…​

  • Array types of Objects, primitives, other Arrays,…​

  • Map types where keys are Strings and values are again anything

Example 20. Nested branches of child configurations
interface DbConfig {
	List<ServerConfig> dbConnections();
	Map<String, String> additionalParameters();
	byte[] privateKey();
	Nested<Timing> defaultTiming();
}

interface Nested<T> {
	boolean active();
	T wrapped();
}
DbConfig dbConfig = ConfijBuilder.of(DbConfig.class)
	.loadFrom(new ExplicitPropertiesSource()
		.set("dbConnections.0.url", "https://db0.example.com")
		.set("dbConnections.0.timing.keepAlive", "30s")
		.set("additionalParameters.somekey", "somevalue"))
	.build();

If the standard Collection interfaces are used ConfiJ will use an immutable representation of it. But any instance can be used as long as it has an empty constructor (but at the loss of immutability). Same holds for Map<String,?> types.

Interface proxies are Java-serializable if all the customized values are serializable, too.

Java Bean types (POJO) are not supported yet. Please contribute.

Templating

All configuration values might contain placeholders referencing other values. Placeholders are only resolved after all sources were loaded. Therefore it is possible for an unfinished configuration to contain undefined references.

Additionally some source paths are subject to templating (see Chapter 5, The AnySource).

10. built-in Templating

Any property can contain placeholders wrapped in ${ }. These referenced properties can themselves reference other properties.

Example 21. Simple Variable replacement
interface Salutation {
	@Default("John")
	String name();

	@Default("Hi ${name}")
	String hello();
}
given:  name=
yields: hello → Hi John

given:  name=Bob
yields: hello → Hi Bob

given:  hello=Cya
yields: hello → Cya

To disable templating support, see Chapter 11, Custom Templating.

10.1. Nested Properties

Variables are always relative to the node that defines them.

Example 22. Referencing nested properties
interface Letter {
	Salutation salutation();

	@Default("I dearly miss you, ${salutation.name}!") (1)
	String body();

	Regards regards();
}

interface Regards {
	@Default("${.salutation.name}'s friend") (2)
	String sender();

	@Default("Sincierly\n${sender}")
	String regards();
}
Letter letter = ConfijBuilder.of(Letter.class).build();
assertThat(letter.regards().sender()).isEqualTo("John's friend");
assertThat(letter.body()).isEqualTo("I dearly miss you, John!");
1 Variables can reference nested properties. Default segment separator is a dot. Note that the reference is still relative to where body is defined.
2 To reference a property relative to the root configuration, prefix it with a dot. In this example Letter is the root.

10.2. Recursive Properties

Variables themselves can be built out of other variables.

Example 23. Recursive Variable replacement
interface Recursive {
	@Default("Alice")
	String party1();
	@Default("Bob")
	String party2();
	@Default("1")
	int victimsId();
	@Default("Poor ${party${victimsId}}!")
	String victim();
}

11. Custom Templating

It is possible register a custom templating functionality globally or per property. One use case is to disable templating per property.

Example 24. Disable templating per property using an annotation
interface Noop {
	@NoopResolver
	String canContainDollar();
}
Example 25. Disable templating for interface and all its children
@NoopResolver
interface GlobalNoop {
	String canContainDollar();
}
Example 26. Disable templating programmatically at initialization
ConfijBuilder.of(BuilderNoop.class).templatingDisabled()

A custom templating engine can be registered in the same ways. Every custom resolver must provide an empty constructor.

Example 27. Custom sample templating
static class FooResolver implements ValueResolver {
	@Override
	public String resolveValue(ConfijNode baseLeaf, String valueToResolve) {
		return "foo";
	}
}

interface CustomResolver {
	@Default("everything becomes foo")
	@VariableResolver(FooResolver.class)
	String everyVariableIsFoo();
}

Post-Processing

12. Non-Null Validator

A recursive non-null validator is part of core. It verifies that the final configuration has all values set. It will not complain about null-values per default. To activate it add any @NotNull or @NonNull annotation to your config definition. Add such an annotation to your configuration root to apply it everywhere as non-null is enforced in all child-configuration values as well. To again allow for null values, use the opposite @Nullable. Any annotation named alike will do as long as it is available at runtime.

Example 28. Non-Null Validator activation
@NotNull
interface NothingIsNull {
	String willCrashIfNull();
	@Nullable Long explicitlyNullable();
	Optional<Integer> allowedToBeEmpty();
}
It is recommended to use Optional<> to explicitly mark optional configuration values instead of @Nullable.

Core provides a @NonNullValidator.Nullable annotation but you can also restrict yourself to Hibernate annotations in case you are using the Chapter 13, JSR303 Validator already.

This validator can be disabled for the whole configuration:

Example 29. Disable the Non-Null Validator globally
ConfijBuilder.of(NothingIsNull.class).validationAllowsNull().build()

13. JSR303 Validator

JSR303 Validator format requires the ch.kk7:confij-validation maven artifact.

Straight forward Hibernate-Validation. Validation is applied to the configuration as a last step after templating.

Example 30. JSR303 Bean validation
interface Jsr303Validated {
	@NotEmpty
	String mandatory();
	@Pattern(regexp = "[A-Z]*")
	String uppercase();
}

The JSR303 validator works together with the Chapter 12, Non-Null Validator, however this means that the semantics of @NonNull is applied recursively to all child-nodes, which is usually desirable for configurations.

ch.kk7:confij-validation depends on hibernate-validator >= 6.1 in order to support non-getter properties, too.

14. Logging

ConfiJ core relies on java.util.logging (JUL) only due to it’s zero-dependency strategy. If ch.kk7:confij-slf4j is found on the classpath, logging uses the slf4j-framework. To support a custom logging-framework, register a Chapter 7, ConfiJ’s ServiceLoaders for ConfijLogFactory.

15. Live Reloading

ConfiJ treats configurations are immutable objects. Using mutable objects like arrays is generally possible, but not advised. In any case ConfiJ will always create a completely new configuration if one of the values changes.

If a reload strategy is used, the ConfiJBuilder.buildWrapper() must be used, as it allows to register to change notifications.

Example 31. Register a reload strategy which will update the configuration every 2 seconds
interface ClientConfig {
	Endpoint database();
	Endpoint backend();
}
interface Endpoint {
	URL url();
	Duration timeout();
	boolean active();
}
ConfijWrapper<ClientConfig> wrapper = ConfijBuilder.of(ClientConfig.class)
	.loadFrom(source)
	.reloadPeriodically(Duration.ofSeconds(2))
	.buildWrapper();
ClientConfig currentConfig = wrapper.get(); // always the most up-to-date value

You chan be notified on a change on any configuration value (it doesn’t have to be the root). If one of the nested configuration values changes, all parent objects will be notified, too. For example if database.active swaps, a change handler on database will trigger too.

Example 32. Register change handlers on individual configuration values
wrapper.getReloadNotifier().registerReloadHandler(
		reloadEvent -> resetHttpClient(reloadEvent.getNewValue()), (1)
		currentConfig.backend()); (2)

wrapper.getReloadNotifier().<Boolean>registerReloadHandler(
		reloadEvent -> setDbActivityTo(reloadEvent.getNewValue()),
              currentConfig.database(), "active"); (3)
1 A handler receives a reload event, whenever this value (or a child/nested value) changes
2 The target object to attach the handler to. Can be any object…​
3 …​except primitives. For which a relative path to a parent object has to be set.