There’s a variety of configuration frameworks to use in our Java applications. Java Config Builder is one of them and it offers some nice features that I would like to demonstrate in the following short examples as are:
Loading values from different sources like property-files, environment variables, command-line-arguments or system properties, specifying default values, mapping arbitrary types or collections, merging configurations and using the Java Bean Validation standard aka JSR-303.
Project Setup
First of all we need to add the config-builder library to our project. Maven or Gradle – it’s your choice (SBT is fine, too ;)
Maven
There is just one dependency that we need to add to our pom.xml:
<dependency>
<groupId>com.tngtech.java</groupId>
<artifactId>config-builder</artifactId>
<version>1.2</version>
</dependency>
Gradle
Preferring Gradle, we might want to add the following dependency to our build.gradle
'com.tngtech.java:config-builder:1.2'
Configuration Sources / Features
First a short explanation of basic configuration types:
Property File
We’re able to load multiple property files and property file locations using the following annotations:
-
@PropertiesFiles( \{“config1″,”confi2″}): Loads a file config1.properties and config2.properties from the class path
-
@PropertyLocations(directories = \{“/some/path”}): Loads property files from the designated directory
-
@PropertySuffixes(): Filter property files by a given suffix
Sample property file for the following example named config.properties and put in src/main/resources:
app.name=Sample Application using Config Builder
System Properties
In addition we may reference system properties using the following annotation:
-
@SystemPropertyValue(“systemproperty”): Loads the system property named systemproperty
Environment Variables
Also we’re able to reference environment variables like this:
-
@EnvironmentVariableValue(“JAVA_HOME”): Loads the environment variable named JAVA_HOME
Command Line Arguments
Finally we’re able to read in command line arguments and specify their format as we’re used to do using tools like Apache CLI or something else:
-
@CommandLineValue(shortOpt = “s”, longOpt = “silent”, hasArg = false): Assigns a command line input. The following formats are allowed: -s, –silent and this input has no further parameters, therefore we set hasArg to false.
-
To make this work, we need to pass the args passed in from our main method to the config builder like this: new ConfigBuilder(..).withCommandLineArgs(args)
Default Values
If we want to define fallback or default values we’re able to do so using the following annotation:
-
@DefaultValue(“value”): Specifies the string value as the default value for the annotated field.
-
This does not sound very interesting at first but combined with type transformers we may assign more complex default values represented as a string.
Type Transformers
Type Transformers allow us to transform the string value read from one of the configuration sources describe above into something more complex to use in our application.
-
@TypeTransformers(MyTransformer.class): Specifies the type transformer to use.
-
The type transformer class simply needs to extends TypeTransformer<I,O> where I is the input type e.g. String and O is the transformed return type e.g. List<Foo>
-
The following transformer taken from the example below transforms a string into a list of user objects:
Our User class:
package com.hascode.tutorial;
public class User {
private String name;
public User(final String name) {
this.name = name;
}
// getter, setter ommitted
}
The transformer using Java 8 streams and lambda expressions. We’re assuming that a string containing one or multiple user names, separated by a comma are passed in.
class StringToUserTransformer extends TypeTransformer<String, List<User>> {
@Override
public List<User> transform(final String input) {
return Stream.of(input.split(",")).map(s -> new User(s))
.collect(Collectors.toList());
}
}
Update
As Andreas Würl kindly remarked, the transformation as written in the type transformer above is not necessary as the framework performs transitive transform steps here so that the following type converter is sufficient to produce the same result:
public static class StringToUserTransformer extends
TypeTransformer<String, User> {
@Override
public User transform(final String input) {
return new User(input);
}
}
Our Custom Configuration
Now let’s put everything together in one configuration class assembling configuration fragments from different sources:
-
We’re loading a properties file named config.properties
-
From this property file we’re loading the value for the key app.name into a string variable named appName
-
We’re loading the environment variableJAVA_HOME into a string variable named javaHomeDir
-
We’re loading a system property named target-os into a string variable named targetOs
-
We’re loading a command line argument (cli) with a default-value of interactive into the string variable named mode. We may specify the cli arg using the notation -m or –mode
-
We’re loading a cli argument without an argument into a boolean property. We may specify the option with -s or with –silent
-
We’re loading a list of users from the command line using a custom type transformer that transforms the input string into a generic list of users
-
We’re loading another list of user but in addition we’re specifying a default value here
-
The type transformer splits the command line argument using a comma as separator token and returns an arraylist of user objects
package com.hascode.tutorial;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.tngtech.configbuilder.annotation.propertyloaderconfiguration.PropertiesFiles;
import com.tngtech.configbuilder.annotation.typetransformer.TypeTransformer;
import com.tngtech.configbuilder.annotation.typetransformer.TypeTransformers;
import com.tngtech.configbuilder.annotation.valueextractor.CommandLineValue;
import com.tngtech.configbuilder.annotation.valueextractor.DefaultValue;
import com.tngtech.configbuilder.annotation.valueextractor.EnvironmentVariableValue;
import com.tngtech.configbuilder.annotation.valueextractor.PropertyValue;
import com.tngtech.configbuilder.annotation.valueextractor.SystemPropertyValue;
@PropertiesFiles("config")
public class Config {
@PropertyValue("app.name")
private String appName;
@EnvironmentVariableValue("JAVA_HOME")
private String javaHomeDir;
@SystemPropertyValue("target-os")
private String targetOs;
@DefaultValue("interactive")
@CommandLineValue(shortOpt = "m", longOpt = "mode", hasArg = true)
private String mode;
@CommandLineValue(shortOpt = "s", longOpt = "silent", hasArg = false)
private boolean silent;
@TypeTransformers(StringToUserTransformer.class)
@CommandLineValue(shortOpt = "u", longOpt = "users", hasArg = true)
private List<User> usersAllowed = new ArrayList<>();
@DefaultValue("burt,bart,allan")
@TypeTransformers(StringToUserTransformer.class)
@CommandLineValue(shortOpt = "f", longOpt = "forbid", hasArg = true)
private List<User> usersForbidden = new ArrayList<>();
public List<User> getUsersForbidden() {
return usersForbidden;
}
public void setUsersForbidden(List<User> usersForbidden) {
this.usersForbidden = usersForbidden;
}
public static class StringToUserTransformer extends
TypeTransformer<String, List<User>> {
@Override
public List<User> transform(final String input) {
return Stream.of(input.split(",")).map(s -> new User(s))
.collect(Collectors.toList());
}
}
// getter, setter ommitted..
}
Running the Application
This sample application initializes our configuration and prints it to the screen.
package com.hascode.tutorial;
import com.tngtech.configbuilder.ConfigBuilder;
public class Main {
public static void main(final String[] args) {
System.setProperty("target-os", "Linux x86_64 GNU/Linux");
Config cnf = new ConfigBuilder<Config>(Config.class)
.withCommandLineArgs(args).build();
System.out.println("Starting application " + cnf.getAppName());
System.out.println("\tJava-Home-Dir\t:" + cnf.getJavaHomeDir());
System.out.println("\tTarget-OS\t:" + cnf.getTargetOs());
System.out.println("\tSelected Mode\t:" + cnf.getMode());
System.out.println("\tTarget-OS\t:" + cnf.isSilent());
System.out.println("\tAllowed users\t:");
cnf.getUsersAllowed().forEach(
u -> System.out.println("\t\t\t" + u.getName()));
System.out.println("\tForbidden users\t:");
cnf.getUsersForbidden().forEach(
u -> System.out.println("\t\t\t" + u.getName()));
}
}
Running with Maven
We’re able to run our Main class with some command line args set running the following command:
mvn clean compile exec:java -Dexec.mainClass=com.hascode.tutorial.Main -DcommandLineArgs="--mode=automatic" "-s" "--users=susan,lisa,tim"
Running from Eclipse IDE
In Eclipse just create a new run configuration and add the following program arguments:
–mode=automatic -s –users=susan,lisa,tim
Output
On my system, running the program produces the following output:
Starting application Sample Application using Config Builder
Java-Home-Dir :/usr/lib/jvm/jdk1.8.0_05
Target-OS :Linux x86_64 GNU/Linux
Selected Mode :automatic
Target-OS :true
Allowed users :
susan
lisa
tim
Forbidden users :
burt
bart
allan
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/config-builder-tutorial.git
Resources
Alternative: Netflix Archaius
I have written another tutorial about a possible alternative that has a strong focus on working with dynamic properties here: “Dynamic Configuration Management with Netflix Archaius and Apache ZooKeeper, Property-Files, JMX”
Article Updates
-
2014-06-18: Added remark from Andreas Würl regarding transitive transform steps in a type converter
-
2016-04-13: Added link to my Netflix Archaius tutorial