Externalize config with INI/YAML and variables
Move settings out of your code into a config file, in INI or YAML, and let variables fill in values from system properties, environment variables and other entries when they are read.
What you'll build
A small program that loads the same settings from app.ini and from app.yaml, reads single entries as typed values, reads a whole section through a Java interface, and overrides the port with -Dapp.port without editing either file.
The program prints:
yaml server section: localhost 8080 http://localhost:8080
with -Dapp.port=9090: http://localhost:9090
Prerequisites and time
- Java 17 or later and Maven 3.
org.apache.juneau:juneau-config:10.0.0-SNAPSHOT, shown in the POM in step 1. It brings injuneau-marshall, which converts entries to Java types.- About 15 minutes.
10.0.0-SNAPSHOT is not on Maven Central, so the POM adds the Apache snapshots repository. See the note in the MCP Quickstart and Install Juneau.
Steps
Put each class in src/main/java/com/example/ and start each file with package com.example;. Any package works if you use it everywhere. The two config files go in src/main/resources/ExternalizeConfig/.
1. Create the project
Create an empty directory with this pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>externalizeconfig</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<juneau.version>10.0.0-SNAPSHOT</juneau.version>
</properties>
<repositories>
<repository>
<id>apache-snapshots</id>
<url>https://repository.apache.org/snapshots/</url>
<releases><enabled>false</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-config</artifactId>
<version>${juneau.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.4</version>
</plugin>
</plugins>
</build>
</project>
2. Write the INI file
Save this as src/main/resources/ExternalizeConfig/app.ini. Sections are in brackets, and a # line before an entry is its comment. The values use three variables:
$S{app.port,8080}reads theapp.portsystem property, defaulting to8080.$E{APP_DB_USER,app}reads theAPP_DB_USERenvironment variable, defaulting toapp.$C{server/host}reads another entry from the same config.
[server]
# Host name clients use to reach the service.
host = localhost
# Defaults to 8080; start the JVM with -Dapp.port=9090 to override.
port = $S{app.port,8080}
# Built from the two entries above.
baseUrl = http://$C{server/host}:$C{server/port}
[database]
# Read from the APP_DB_USER environment variable, falling back to "app".
user = $E{APP_DB_USER,app}
poolSize = 10
3. Or write the same settings in YAML
Save this as src/main/resources/ExternalizeConfig/app.yaml. Top-level keys are sections, nested keys are entries, and the variables work the same way. You need only one of the two files; this guide loads both to show they read the same.
server:
# Host name clients use to reach the service.
host: localhost
# Defaults to 8080; start the JVM with -Dapp.port=9090 to override.
port: $S{app.port,8080}
# Built from the two entries above.
baseUrl: http://$C{server/host}:$C{server/port}
database:
# Read from the APP_DB_USER environment variable, falling back to "app".
user: $E{APP_DB_USER,app}
poolSize: 10
4. Describe a section as an interface
Juneau can implement an interface over a section for you: each getter reads the entry with the matching name and converts it to the return type.
/**
* Typed view of the [server] section; Juneau implements it for you.
*/
public interface AppSettings {
String getHost();
int getPort();
String getBaseUrl();
}
5. Load and read the config
import org.apache.juneau.config.*;
import org.apache.juneau.config.store.*;
/**
* Loads the same settings from INI and from YAML, reads them three ways, and shows a system-property override.
*/
public class ConfigDemo {
public static void main(String[] args) throws Exception {
System.out.println(run());
}
public static String run() throws Exception {
// The file extension picks the format; the classpath store reads from src/main/resources.
var ini = Config.create("ExternalizeConfig/app.ini").store(ClasspathStore.DEFAULT).build();
var yaml = Config.create("ExternalizeConfig/app.yaml").store(ClasspathStore.DEFAULT).build();
// One key at a time, converted to the type you ask for.
var port = ini.get("server/port").asInteger().orElse(0);
var user = ini.get("database/user").asString().orElse(null);
var poolSize = yaml.get("database/poolSize").asInteger().orElse(0);
// A whole section through an interface.
var server = yaml.getSection("server").asInterface(AppSettings.class).orElseThrow();
// $S{app.port,8080} is resolved every time the value is read.
System.setProperty("app.port", "9090");
String overridden;
try {
overridden = ini.get("server/baseUrl").asString().orElse(null);
} finally {
System.clearProperty("app.port");
}
return String.join("\n",
"ini server/port: " + port,
"ini database/user: " + user,
"yaml database/poolSize: " + poolSize,
"yaml server section: " + server.getHost() + " " + server.getPort() + " " + server.getBaseUrl(),
"with -Dapp.port=9090: " + overridden);
}
}
Run it
From the project directory:
mvn -q compile exec:java -Dexec.mainClass=com.example.ConfigDemo
Expected output, unless APP_DB_USER is set in your environment:
ini server/port: 8080
ini database/user: app
yaml database/poolSize: 10
yaml server section: localhost 8080 http://localhost:8080
with -Dapp.port=9090: http://localhost:9090
How it works
- One API, two formats.
Config.create(name)picks the format from the file extension:.yamlor.ymlis YAML, anything else is INI. Both produce the same sections and entries, so the code that reads them doesn't change. See INI Config Format and YAML Config Files. - Where the file comes from.
ClasspathStorereads from the classpath, which suits defaults packaged with the application. The default store,FileStore.DEFAULT, reads files from the working directory instead and can save changes back. See Config Stores. - Variables resolve on read. Entries keep the
$S{...},$E{...}and$C{...}text. Juneau resolves them each time you read the entry, which is why settingapp.portafter loading still changesbaseUrl. The default after the comma applies when the property or variable is missing. See Variables and the Simple Variable Language. - Typed reads.
get(key)returns an entry;asInteger(),asString()and the otheras...methods convert it and return anOptional, empty when the entry is missing.getSection(name).asInterface(type)maps a whole section.
Next steps
- Use config values in a REST resource with
$C{...}in annotations, then follow Build a REST API on Spring Boot. - Read more about sections, imports, listeners and saving changes: juneau-config.
- Control how your own types are written: Serialize a type Juneau doesn't know.