Skip to main content

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 in juneau-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 the app.port system property, defaulting to 8080.
  • $E{APP_DB_USER,app} reads the APP_DB_USER environment variable, defaulting to app.
  • $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: .yaml or .yml is 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. ClasspathStore reads 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 setting app.port after loading still changes baseUrl. 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 other as... methods convert it and return an Optional, empty when the entry is missing. getSection(name).asInterface(type) maps a whole section.

Next steps​