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:
- Per-thread store (if set via
setLocal()) - Global store (if set via
setGlobal()) - Sources added at runtime via
addSource(), newest first Config(SPI priority 100)- Command-line args (50)
- System properties (40)
- Environment variables, verbatim name (30)
.envfile (20)- Manifest entries (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 thejuneau.dotenv.pathsystem property orJUNEAU_DOTENV_PATHenvironment variable.RelaxedPropertySource — a decorator that wraps a delegate source and retries a miss with canonical name variants (upper-underscore and lower-dotted), soMySection/myKey,my.key,myKey, andMY_SECTION_MY_KEYcan all resolve the same logical property. The verbatim name is always tried first.ArgsPropertySource — backed by parsed command-lineArgs, supporting lookups by flag name or positional index.ArgsPropertySource.createDefault()derives the args fromsun.java.command(falling back to thejuneau.argssystem property).ManifestFilePropertySource — backed by a classpathMETA-INF/MANIFEST.MF(viaManifestFile), 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 SpringEnvironmentinto theSettingssingleton as aPropertySource, with a bounded cache of mixed-case keys Spring does not have.SpringBeanStore — installs it, one perApplicationContext, 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) orJUNEAU_SETTINGS_DISABLEGLOBAL(system env) - If set totrue, prevents new global overrides from being set viasetGlobal(). Existing global overrides will still be returned until explicitly removed.juneau.enableVerboseExceptions- Controls the opt-inThrowableUtils.log(exception)helper — it is never invoked automatically by any exception factory (iaex,brex,exex, etc.). If set totrue, callinglog(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("...")); whenfalse(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
@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 —
findreturns empty andexistsreturnsfalse.
Built-in stores
InMemorySecretStore — the zero-config default: aConcurrentHashMap-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 satisfyingfind/existsfromSystem.getenv.store/deletethrowUnsupportedOperationException. 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
| Method | Contract |
|---|---|
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 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
PropertySourceProvider:
SecretStore create()— build the store, or returnnullto skip registration (for example on an unsupported OS).default int order()— sort key, lowest first. The default is0.
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:
| Provider | Store | order() |
|---|---|---|
KeychainSecretStoreProvider (juneau-secret-macos-keychain) | KeychainSecretStore (macOS only; returns null elsewhere) | 10 |
EnvVarSecretStoreProvider (juneau-commons) | EnvVarSecretStore | 30 |
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,
PropertySource — a conscious, greppable opt-in (for example wired as a session-scoped
PropertySource[] bean). It implements
toString() paths honor to redact bridged values.
OS keychain-backed store
The opt-in juneau-secret-macos-keychain module ships a
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
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 method | security 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/securitycannot be run, a call is a backend failure and is resolved by theFailMode. - Absent is not a failure. A key that is not in the keychain makes
findreturn empty andexists/deletereturnfalseregardless ofFailMode. - 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; withFAIL_OPEN,findreturns empty,exists/deletereturnfalse, andstoresilently does nothing. - Secret values. A secret passed to
storemust not contain a newline (\n) or carriage return (\r); the call throws anIllegalArgumentExceptionbefore invoking any process.findreturns achar[]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.