@ConfigProperties Annotation Basics
The @ConfigProperties
annotation (introduced in 10.0.0, package
org.apache.juneau.commons.inject) is a
declarative, prefix-scoped whole-object config binder — Juneau's analog of Spring Boot's
@ConfigurationProperties.
Where @Value resolves a single configuration value onto a single field or
parameter, @ConfigProperties resolves an entire prefix (myapp.host, myapp.port, myapp.timeout, ...) onto
the fields of one POJO in a single pass:
import org.apache.juneau.commons.inject.ConfigProperties;
@ConfigProperties(prefix = "MyService")
public class MyServiceProperties {
public String host = "localhost";
public int port = 8080;
}
The two annotations are complementary, not competing — a class can carry @Value fields internally while also
being the target of another class's nested @ConfigProperties bind (see
Nested config objects below), and @ConfigProperties fields that are themselves
annotated @Value/@Inject are left untouched by the config bind (see
Coexistence with @Value and @Inject).
Minimal end-to-end example
Given the MyServiceProperties class above, materializing it through
BeanInstantiator
runs the bind automatically:
BeanStore store = new BasicBeanStore(null);
MyServiceProperties props = BeanInstantiator.of(MyServiceProperties.class, store).run();
With MyService.host=example.com and MyService.port=9090 set as system properties (or environment variables,
or any other registered PropertySource),
props.host is "example.com" and props.port is 9090. A key that isn't present anywhere in the chain leaves
the field's initializer value untouched — bind-only-present semantics, the same "default lives in the field
initializer" idea @Value's ${key:default} syntax expresses inline.
You can also invoke the underlying binder directly, with or without the annotation, via ConfigPropertiesBinder's fluent builder:
MyServiceProperties props = ConfigPropertiesBinder.of(new MyServiceProperties(), "MyService")
.relaxed(true) // default
.run();
Relaxed key matching
relaxed() defaults to true on the annotation (and on the binder). For each field, the candidate key
prefix.fieldName is probed in up to three spellings, via
RelaxedPropertySource.candidates(String) —
the same relaxed-matching logic used elsewhere in juneau-commons:
- Verbatim —
MyService.host SCREAMING_SNAKE_CASE—MY_SERVICE_HOST(the env-var-friendly spelling)lower.dotted—my.service.host
This is what lets a Kubernetes-style environment variable (MY_SERVICE_HOST=example.com) satisfy a
camelCase-prefixed key without any extra configuration. Set relaxed = false on the annotation (or
.relaxed(false) on the binder) to require an exact, verbatim key match only.
Source precedence
Each candidate spelling is resolved through the same chain @Value uses — candidate-major: every spelling
is checked against the entire chain before the next spelling is tried, in this order per candidate:
- A
Settings.setLocal(...)/Settings.setGlobal(...)test override, if one is recorded for that exact candidate — always wins. - Any caller-scoped PropertySource-typed
beans registered in a
BeanStorepassed viaConfigPropertiesBinder.of(...).beanStore(...)(or supplied automatically by the@ConfigPropertiesresolution hook — see below) — consulted ahead of the globalSettingschain, first-match-wins among the scoped sources. - The process-wide Settings.get(name)
chain — registered
PropertySources, system properties, then system environment variables.
Because precedence is candidate-major rather than source-major, a verbatim hit in a lower-priority source can outrank a relaxed-spelling override set on a higher-priority source, if that override used a different spelling than the one that resolved first. In practice this rarely matters — most deployments pick one spelling convention per key — but it's worth knowing if a key is set two different ways in two different places.
The caller-scoped PropertySource layer is exactly how the RestContext pilot (see
RestContext) lets a resource's @Rest(config=...) file participate in binding
RestContext.* keys without mutating the shared Settings.get() singleton.
Nested config objects
A field whose declared type is itself @ConfigProperties-annotated is bound recursively, using
prefix.fieldName as the sub-prefix — the field's own @ConfigProperties(prefix=...) value is ignored for a
nested bind (the enclosing prefix always wins):
@ConfigProperties(prefix = "unused-when-nested")
public class ServerProperties {
public String host = "localhost";
public int port = 80;
}
@ConfigProperties(prefix = "MyApp")
public class MyAppProperties {
public ServerProperties server = new ServerProperties();
}
MyApp.server.host and MyApp.server.port bind onto server's fields. A null nested field is only
materialized (via an accessible no-arg constructor) if at least one key resolves anywhere under its sub-prefix —
otherwise it's left null, so an optional nested block doesn't force an empty instance into existence. Circular
@ConfigProperties nesting (a type that, directly or transitively, nests itself) is detected and raises a
RuntimeException rather than recursing indefinitely.
This nested-field detection is inheritance-aware: a field whose declared type is a subclass of an
@ConfigProperties-annotated type still triggers a nested bind. This is different from the annotation's own
top-level trigger — see the note below.
Coexistence with @Value and @Inject
@ConfigProperties and @Value serve different granularities and coexist on
the same class without conflict. A field already annotated @Value or @Inject is never bound by
ConfigPropertiesBinder, even when a matching prefix.fieldName key exists — injection resolution owns those
fields exclusively:
@ConfigProperties(prefix = "MyService")
public class MyServiceProperties {
public String host = "localhost"; // bound by ConfigPropertiesBinder
@Value("${MyService.label:default-label}")
public String label; // bound by @Value, never touched by the config bind
}
When materialized through BeanInstantiator, the whole-object config bind runs after @Value/@Inject
field-and-method injection and before any @PostConstruct method, so a @PostConstruct hook observes
fully-bound config fields either way.
Auto-registration in the BeanStore
When a @ConfigProperties-annotated class is materialized through BeanInstantiator, the bound instance is
automatically registered into the bean store as a framework default (a non-clobbering
addDefaultSupplier — lowest priority, so an explicit caller registration still wins), under the
declared annotated type. It's then retrievable anywhere downstream:
MyServiceProperties props = beanStore.getBean(MyServiceProperties.class).orElseThrow();
or injected directly:
public class MyResource {
@Inject
MyServiceProperties props;
}
Not @Inherited: the BeanInstantiator trigger fires only when the exact declared type being
instantiated carries @ConfigProperties — a subclass or interface implementor of an annotated type does not
itself trigger binding or auto-registration. (This is distinct from the nested-field detection described above,
which is inheritance-aware.)
Current limitations
- No
Optional<T>/Supplier<T>field types. Unlike@Value, type conversion here is keyed on the field's rawClass, not its full genericType— generic container types aren't supported as@ConfigPropertiesfield types. Use a plain field type (e.g.String uriAuthoritywith anulldefault, rather thanOptional<String>) if you need "unset" to be distinguishable from a real value. - Validation is currently a no-op seam. A post-bind extension point,
ConfigPropertiesValidator,
is invoked once per bound object (innermost-nested first) — but the shipped default,
ConfigPropertiesValidator.NO_OP, performs no validation. It exists purely as a placement marker so a future release can add JSR-303-style validation without changing the binder's public shape.
See also
@ValueAnnotation Basics — the single-value counterpart.- Inject Package — the broader
org.apache.juneau.commons.injectinjection model. - RestContext —
RestContextProperties, the real-world@ConfigPropertiespilot. - org.apache.juneau.commons.inject.ConfigProperties
- org.apache.juneau.commons.inject.ConfigPropertiesBinder
- org.apache.juneau.commons.inject.ConfigPropertiesValidator
- org.apache.juneau.commons.settings.RelaxedPropertySource