Skip to main content

Settings Package

The org.apache.juneau.commons.settings package provides a thread-safe way to access system properties with support for global and per-thread overrides, making it useful for unit tests and configuration management.

Key Classes​

Settings​

Encapsulates Java system properties with support for global and per-thread overrides.

// Get the singleton Settings instance once and reuse it
Settings settings = Settings.get();

// Get a system property as a StringSetting
StringSetting setting = settings.get("my.property");
String value = setting.get(); // Get the string value

// Get with type conversion using StringSetting methods
Setting<Integer> intSetting = settings.get("my.int.property").asInteger();
Setting<Long> longSetting = settings.get("my.long.property").asLong();
Setting<Boolean> boolSetting = settings.get("my.bool.property").asBoolean();
Setting<Double> doubleSetting = settings.get("my.double.property").asDouble();
Setting<Float> floatSetting = settings.get("my.float.property").asFloat();
Setting<File> fileSetting = settings.get("my.file.property").asFile();
Setting<Path> pathSetting = settings.get("my.path.property").asPath();
Setting<URI> uriSetting = settings.get("my.uri.property").asURI();
Setting<Charset> charsetSetting = settings.get("my.charset.property").asCharset();

// Use custom type conversion
Setting<MyCustomType> customSetting = settings.get("my.custom.property")
.asType(MyCustomType.class);

// Reset a setting to force recomputation
setting.reset();

Per-Thread Overrides​

// Override for current thread (useful in unit tests)
Settings.get().setLocal("my.property", "test-value");
// ... test code that uses the override ...
Settings.get().unsetLocal("my.property"); // Remove specific override
// OR
Settings.get().clearLocal(); // Clear all thread-local overrides

Global Overrides​

// Set global override (applies to all threads)
Settings.get().setGlobal("my.property", "global-value");
// ... code that uses the global override ...
Settings.get().unsetGlobal("my.property"); // Remove specific override
// OR
Settings.get().clearGlobal(); // Clear all global overrides

Custom Settings Instance​

// Create a custom Settings instance with custom sources
MapStore springSource = new MapStore();
springSource.set("spring.datasource.url", "jdbc:postgresql://localhost/db");

Settings custom = Settings.create()
.addSource(springSource)
.addSource(FunctionalPropertySource.of(System::getProperty))
.build();

The springSource above is just a generic illustrative example (a plain MapStore populated with a Spring-style key) — it is not the actual Spring adapter. See Spring Property Integration below for the real integration.

Lookup Order​

When retrieving a property value, the lookup order is:

  1. Per-thread store (if set via setLocal())
  2. Global store (if set via setGlobal())
  3. Sources added at runtime via addSource(), newest first
  4. Config (SPI priority 100)
  5. Command-line args (50)
  6. System properties (40)
  7. Environment variables, verbatim name (30)
  8. .env file (20)
  9. Manifest entries (10)
  10. Relaxed environment variable names (always last)

If no registered source supplies system properties, a fallback system-property source sits just above the relaxed environment source.

The environment, .env, manifest and args sources cache their lookups, since their values do not change during a run. System properties and runtime-added sources are read live, so changes to them are seen immediately.

Property Sources and Stores​

PropertySource​

Provides read-only access to property values.

// Functional source
PropertySource source = FunctionalPropertySource.of(System::getProperty);

// Map-based source
MapStore store = new MapStore();
store.set("key", "value");
PropertySource source2 = store; // Stores extend PropertySource

PropertyStore​

Provides read/write access to property values.

// Map store
MapStore store = new MapStore();
store.set("key", "value");
String value = store.get("key");

// Functional store
FunctionalPropertyStore store2 = FunctionalPropertyStore.of(
key -> getValue(key), // Getter
(key, value) -> setValue(key, value), // Setter
key -> removeValue(key), // Unsetter
() -> clearAll() // Clear
);

Built-in Property Sources​

Beyond the generic FunctionalPropertySource/MapStore shown above, juneau-commons ships several purpose-built PropertySource implementations, each with a *Provider companion for ServiceLoader-driven auto-registration:

  • DotenvPropertySource — reads key-value pairs from a .env-style file. Defaults to ./.env; override the path via the juneau.dotenv.path system property or JUNEAU_DOTENV_PATH environment variable.
  • RelaxedPropertySource — a decorator that wraps a delegate source and retries a miss with canonical name variants (upper-underscore and lower-dotted), so MySection/myKey, my.key, myKey, and MY_SECTION_MY_KEY can all resolve the same logical property. The verbatim name is always tried first.
  • ArgsPropertySource — backed by parsed command-line Args, supporting lookups by flag name or positional index. ArgsPropertySource.createDefault() derives the args from sun.java.command (falling back to the juneau.args system property).
  • ManifestFilePropertySource — backed by a classpath META-INF/MANIFEST.MF (via ManifestFile), memoized so the classpath is scanned at most once per source instance. ManifestFilePropertySource.createDefault() uses default classloader scanning.
// Wrap the system-env source so env vars bind relaxedly (e.g. MY_PROP satisfies my.prop).
Settings settings = Settings.create()
.addSource(new RelaxedPropertySource(Settings.SYSTEM_ENV_SOURCE))
.addSource(new DotenvPropertySource())
.addSource(ArgsPropertySource.createDefault())
.build();

Spring Property Integration​

juneau-commons itself has no Spring dependency — the Settings framework only defines the pluggable PropertySource/PropertyStore SPI shown above; the first-class Spring integration lives in the juneau-rest-server-springboot module.

  • SpringEnvironmentPropertySource — adapts a Spring Environment into the Settings singleton as a PropertySource, with a bounded cache of mixed-case keys Spring does not have.
  • SpringBeanStore — installs it, one per ApplicationContext, and removes it when the context closes or the last store on it is cleared, so Juneau's @Value("${...}") injection resolves against Spring's property space (application.yaml/properties, env vars, command-line args, etc.).
  • For details, see juneau-rest-server-springboot.

StringSetting​

A specialized Setting for string values that provides convenience methods for type conversion.

StringSetting setting = Settings.get().get("my.property");

// Type conversions
Setting<Integer> intValue = setting.asInteger();
Setting<Long> longValue = setting.asLong();
Setting<Boolean> boolValue = setting.asBoolean();
Setting<Double> doubleValue = setting.asDouble();
Setting<Float> floatValue = setting.asFloat();
Setting<File> fileValue = setting.asFile();
Setting<Path> pathValue = setting.asPath();
Setting<URI> uriValue = setting.asURI();
Setting<Charset> charsetValue = setting.asCharset();

// Custom type conversion
Setting<MyType> customValue = setting.asType(MyType.class);

// Reset to force recomputation
setting.reset();

Common Patterns​

Unit Testing with Thread-Local Overrides​

@Test
void testWithOverride() {
// Override system property for this test
Settings.get().setLocal("my.property", "test-value");

try {
// Test code that uses the property
String value = Settings.get().get("my.property").get();
assertEquals("test-value", value);
} finally {
// Clean up
Settings.get().clearLocal();
}
}

Configuration with Custom Sources​

// Create settings with multiple sources
MapStore configFile = new MapStore();
// Load from config file
configFile.set("app.name", "MyApp");
configFile.set("app.version", "1.0.0");

Settings settings = Settings.create()
.addSource(configFile) // Check config file first
.addSource(FunctionalPropertySource.of(System::getProperty)) // Then system properties
.build();

String appName = settings.get("app.name").get();

System Properties​

  • juneau.settings.disableGlobal (system property) or JUNEAU_SETTINGS_DISABLEGLOBAL (system env) - If set to true, prevents new global overrides from being set via setGlobal(). Existing global overrides will still be returned until explicitly removed.
  • juneau.enableVerboseExceptions - Controls the opt-in ThrowableUtils.log(exception) helper — it is never invoked automatically by any exception factory (iaex, brex, exex, etc.). If set to true, calling log(exception) logs the throwable's stack trace as a warning (not automatically printed to stderr) and returns the same throwable unchanged, so it can be used inline, e.g. throw log(iaex("...")); when false (the default), log(...) is a no-op passthrough. This is useful during development and debugging to pinpoint exactly where an exception originates, even if it is later caught and swallowed. See JuneauCommonsUtils for details.

Secrets​

Where a PropertySource is a read-only source of ordinary configuration values, a SecretStore (package org.apache.juneau.commons.secret, @since 10.0.0) is its secure, mutable sibling — it adds write/delete plus sensitivity semantics. Secret values are held as char[] (never String, so they can be explicitly zeroed and do not linger in the string pool) and are never toString()'d, logged, or dumped.

SecretStore store = new InMemorySecretStore();
store.store("db.password", "hunter2".toCharArray());

boolean present = store.exists("db.password"); // presence, without materializing the value
Optional<char[]> secret = store.find("db.password"); // the value, when present
boolean removed = store.delete("db.password"); // whether a value was present

The four operations model a clean three-state presence distinction:

  • find(key) — returns the secret value when present, Optional.empty() when absent. The only method that materializes the value.
  • exists(key) — reports presence without retrieving (or, for backends that decrypt on read, decrypting) the value.
  • absent — find returns empty and exists returns false.

Built-in stores​

  • InMemorySecretStore — the zero-config default: a ConcurrentHashMap-backed, process-local store. It is not persistent and not cross-node — a convenience default and test double, not a production secret backend. It defensively copies values in and out so callers can zero their own arrays.
  • EnvVarSecretStore — a read-only store satisfying find/exists from System.getenv. store/delete throw UnsupportedOperationException. Gives 12-factor deployments a zero-dependency secret source.

Both are JDK-only. Anything that reaches an OS keychain, a remote vault, or a cloud secret manager lives in a separate opt-in module (see OS keychain below), never in juneau-commons.

Selecting the active store​

Mirroring the rest of Juneau's framework-owned wiring, the active store resolves from a BeanStore, defaulting to an InMemorySecretStore when none has been contributed:

SecretStore store = SecretStores.resolve(beanStore);   // contributed bean wins, else InMemorySecretStore

To contribute a store, add it to the BeanStore under the SecretStore type:

BeanStore beanStore = new BasicBeanStore().addBean(SecretStore.class, new EnvVarSecretStore());
SecretStore store = SecretStores.resolve(beanStore); // the EnvVarSecretStore

Passing a null bean store to resolve(...) also returns a new InMemorySecretStore.

The SecretStore SPI​

SecretStore is a four-method interface. To back secrets with your own system (a database, a vault, a cloud secret manager), implement it:

MethodContract
void store(String key, char[] secret)Store or replace the secret. The caller keeps ownership of secret and may zero it afterward, so an implementation that retains the value must copy it. Read-only stores throw UnsupportedOperationException.
Optional<char[]> find(String key)Return the value, or Optional.empty() if absent. The returned array belongs to the caller (it must not be a live view of internal state) and the caller should zero it when done.
boolean exists(String key)Presence check that must not materialize the value.
boolean delete(String key)Remove the secret. Returns true if one was present and removed, false if the key was already absent. Read-only stores throw UnsupportedOperationException.

An implementation must never toString(), log, or otherwise dump a secret value. A key that is simply absent is a normal result, not a failure. For backends that can be unreachable, let the consumer choose the policy through a FailMode rather than baking one into the store: FAIL_OPEN treats a backend failure as an absent secret (find returns empty, exists/delete return false), while FAIL_CLOSED propagates the error.

public class MyVaultSecretStore implements SecretStore {

private final VaultClient client; // your backend client
private final FailMode failMode;

public MyVaultSecretStore(VaultClient client, FailMode failMode) {
this.client = client;
this.failMode = failMode;
}

@Override
public void store(String key, char[] secret) {
client.put(key, secret);
}

@Override
public Optional<char[]> find(String key) {
try {
return Optional.ofNullable(client.get(key));
} catch (RuntimeException e) {
if (failMode == FailMode.FAIL_OPEN)
return Optional.empty();
throw e;
}
}

@Override
public boolean exists(String key) {
try {
return client.contains(key);
} catch (RuntimeException e) {
if (failMode == FailMode.FAIL_OPEN)
return false;
throw e;
}
}

@Override
public boolean delete(String key) {
return client.remove(key);
}
}

Classpath contribution with SecretStoreProvider​

For classpath contribution there is a parallel SecretStoreProvider SPI, mirroring PropertySourceProvider:

  • SecretStore create() — build the store, or return null to skip registration (for example on an unsupported OS).
  • default int order() — sort key, lowest first. The default is 0.

Register the provider class name in a META-INF/services/org.apache.juneau.commons.secret.SecretStoreProvider file. Discovery is the opt-in SecretStores.fromServiceLoader() — it is deliberately not consulted by resolve(...), so the no-contribution default stays a deterministic InMemorySecretStore rather than silently becoming whatever store happens to be on the classpath. It sorts the providers by order(), calls create() on each in that order, and returns the first non-null store as an Optional<SecretStore> (empty if there is none). To use a classpath-contributed store, seed your bean store from it:

SecretStore store = SecretStores.fromServiceLoader().orElseGet(InMemorySecretStore::new);
BeanStore beanStore = new BasicBeanStore().addBean(SecretStore.class, store);

The providers shipped with Juneau are:

ProviderStoreorder()
KeychainSecretStoreProvider (juneau-secret-macos-keychain)KeychainSecretStore (macOS only; returns null elsewhere)10
EnvVarSecretStoreProvider (juneau-commons)EnvVarSecretStore30

So when both are on the classpath on macOS, the keychain store is chosen; otherwise the environment-variable store. There is no provider for InMemorySecretStore.

Opt-in bridge to $P{...} / @Value​

By design a SecretStore is not part of the general config/SVL namespace — that is what keeps a secret one careless Settings dump or debug log away from disclosure. Where you do want a secret resolvable as a property, SecretStorePropertySource exposes a chosen store as a PropertySource — a conscious, greppable opt-in (for example wired as a session-scoped PropertySource[] bean). It implements SensitivePropertySource, a marker that dump/log/toString() paths honor to redact bridged values.

OS keychain-backed store​

The opt-in juneau-secret-macos-keychain module ships a KeychainSecretStore backed by the macOS security keychain CLI. Because it shells out to an external OS process, it lives outside juneau-commons (which holds the line on zero-runtime-deps / no-OS-integration). It honors a FailMode (fail-open vs fail-closed, modeled on ReplayCache's FailMode) for backend-unavailable behavior, and registers a KeychainSecretStoreProvider for ServiceLoader discovery on macOS.

Dependency​

Artifact org.apache.juneau:juneau-secret-macos-keychain (it depends on juneau-commons), package org.apache.juneau.secret.macos.keychain:

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-secret-macos-keychain</artifactId>
<version>${juneau.version}</version>
</dependency>

How it maps to the keychain​

Each operation runs /usr/bin/security for a generic password item. The item's service (-s) is the service name you pass to the constructor, and its account (-a) is the secret key:

SecretStore methodsecurity subcommand
store(key, secret)add-generic-password -U (create or update)
find(key)find-generic-password -w
exists(key)find-generic-password (no password retrieval)
delete(key)delete-generic-password

Choose a stable, collision-resistant service name (for example a reverse-DNS string) so unrelated applications do not overwrite each other's entries.

Creating a store​

// Default: FailMode.FAIL_CLOSED and a 15 second per-call timeout (DEFAULT_TIMEOUT_SECONDS).
SecretStore store = new KeychainSecretStore("com.example.myapp");

// Choose the failure policy.
SecretStore lenient = new KeychainSecretStore("com.example.myapp", FailMode.FAIL_OPEN);

// Also set the per-call timeout, in seconds (must be > 0).
SecretStore custom = new KeychainSecretStore("com.example.myapp", FailMode.FAIL_CLOSED, 30);

The service name must not be null or blank.

store.store("api.token", "s3cr3t".toCharArray());

if (store.exists("api.token")) {
char[] token = store.find("api.token").orElseThrow();
try {
// use the token...
} finally {
Arrays.fill(token, '\0'); // zero it when done
}
}

store.delete("api.token");

Behavior notes​

  • Platform. Only macOS is supported. On any other OS, or if /usr/bin/security cannot be run, a call is a backend failure and is resolved by the FailMode.
  • Absent is not a failure. A key that is not in the keychain makes find return empty and exists/delete return false regardless of FailMode.
  • Failures. A missing tool, an unexpected non-zero exit, or a timeout is a backend failure. With FAIL_CLOSED (the default) it is thrown as an exception; with FAIL_OPEN, find returns empty, exists/delete return false, and store silently does nothing.
  • Secret values. A secret passed to store must not contain a newline (\n) or carriage return (\r); the call throws an IllegalArgumentException before invoking any process. find returns a char[] that the caller owns and should zero.

ServiceLoader discovery​

KeychainSecretStoreProvider is registered in META-INF/services/org.apache.juneau.commons.secret.SecretStoreProvider with order() 10. Its create() returns null unless the os.name system property contains "mac"; otherwise it creates a KeychainSecretStore with FAIL_CLOSED. The service name is taken from the juneau.secret.macos.keychain.service system property (KeychainSecretStoreProvider.SERVICE_PROPERTY), defaulting to org.apache.juneau (KeychainSecretStoreProvider.DEFAULT_SERVICE).

// With juneau-secret-macos-keychain on the classpath (and running on macOS):
System.setProperty("juneau.secret.macos.keychain.service", "com.example.myapp");
SecretStore store = SecretStores.fromServiceLoader().orElseGet(InMemorySecretStore::new);

Because discovery is opt-in, SecretStores.resolve(beanStore) never picks this store up by itself; add it to the BeanStore explicitly as shown above.

See Also​