Skip to main content

@Value Annotation Basics

The @Value annotation (introduced in 10.0.0, package org.apache.juneau.commons.inject) is a declarative way to inject configuration values into beans created by BeanInstantiator — constructor parameters, setter parameters, and fields.

It complements @Inject:

  • @Inject requests a bean from the bean store by type.
  • @Value requests a configuration value from Settings via the VarResolver.DEFAULT resolver.

The two are mutually exclusive on the same injection site — applying both raises BeanCreationException.

The ${...} shortcut​

Inside a @Value expression (or anywhere you can write SVL), ${key} is shorthand for $P{key} — the PropertyVar that looks up a unified property by name from Settings. The shortcut also accepts a Spring-style default after a colon:

${some.key}                — required key, missing → empty string
${some.key:fallback} — default-when-missing
${section/sub.key} — nested property names (Config sections, etc.)
${a.b:${other:none}} — nested expansion (defaults composed)

This is purely surface syntax — the tokenizer rewrites ${...} to $P{...} before resolution. Everything $P{...} does (recursive resolution, default values, env-var fallback through chained sources) works identically with ${...}. See Simple Variable Language Basics and Variable Basics for the underlying mechanics.

Resolution order (precedence)​

For most @Value sites, resolution goes through the process-wide Settings.get(name) chain: Settings walks every registered PropertySource in reverse insertion order — the most-recently-added source wins. The default chain (built up at startup) looks like:

  1. PropertySourceProvider SPI sources added at process startup (e.g. classpath juneau.cfg via ConfigPropertySourceProvider). These register first and therefore sit at the bottom of the priority list.
  2. Per-microservice Config — org.apache.juneau.microservice.Microservice installs a ConfigPropertySource for its own Config in its constructor.
  3. Spring Environment bridge — when juneau-rest-server-springboot's SpringBeanStore is constructed, it installs a SpringEnvironmentPropertySource so ${spring.app.key} resolves against application.yaml, env vars, command-line args, and any custom Spring PropertySource.
  4. Built-in Settings sources — system properties, environment variables, .env file, argument list, JAR manifest entries.

When the @Value site is on a bean that the REST runtime instantiates (resource bean fields, constructor parameters, request-scoped bean fields), the resolution order grows one tier — the resource's @Rest(config=...) Config participates between the Settings override layer and the chain above.

Because all sources are checked, the same @Value expression resolves identically under a microservice, a Spring Boot app, or a bare REST resource — the origin of the value changes, the expression doesn't.

Field injection​

import org.apache.juneau.commons.inject.Value;

public class MyResource {

@Value("${myapp.title:Untitled}")
private String title;

@Value("${myapp.maxPageSize:50}")
private int maxPageSize;
}

Settings.toType(...) coerces the resolved string to the target type. Coercion supports:

  • Primitives and their boxed wrappers.
  • java.time types (Instant, Duration, Period, LocalDate, etc.) — any type with a parse(CharSequence) or parse(String) static method.
  • Optional<T> — Optional.empty() is injected when the key resolves to null or the empty string; otherwise Optional.of(coerced).
  • Any type with a single-string-argument constructor.

Constructor + setter parameter injection​

@Value works on constructor parameters and on setter (and any method) parameters:

public class JwtValidator {

private final Duration ttl;

public JwtValidator(@Value("${jwt.cache.ttl:PT5M}") Duration ttl) {
this.ttl = ttl;
}

@Inject
public void setKeyResolver(KeyResolver keys) { ... } // @Inject path — bean lookup

public void setMaxClock(@Value("${jwt.clock.skew:PT30S}") Duration skew) { ... }
}

Supplier<String> field type — re-evaluating reads (10.0.0)​

Declaring a @Value field as Supplier<String> instead of String makes the SVL expression re-evaluate on every .get() call against a fresh VarResolverSession. No annotation flag is required — the field type IS the opt-in signal.

public class JwtValidator {

// String field: resolves ONCE at bean construction. Captures whatever Settings/PropertyVar
// returned at injection time and holds that value for the bean's lifetime.
@Value("${jwt.cache.ttl:PT5M}")
String ttl;

// Supplier<String> field: re-evaluates on every .get() call against a fresh session.
// Picks up live Settings.set(...) updates, file-watch reloads, Spring env changes, etc.
@Value("${jwt.cache.ttl:PT5M}")
Supplier<String> liveTtl;
}

The returned Supplier is threadsafe — each .get() opens its own VarResolverSession, so it can be cached on shared fields or handed across threads. (Internally this delegates to VarTemplate.asSupplierWithFreshSessions(...).)

Literal-only expressions (no variables) take a constant-folding fast path: the resolved string is captured once at compile time and every .get() returns the same cached value with no session overhead.

Composition with #{...} functions​

Combining the Supplier<String> field type with the #{...} function catalog produces concise live-factory patterns:

// Request-ID factory — fresh UUID per request.
@Value("#{uuid()}")
Supplier<String> requestId;

// Idempotency key for a downstream API — random per call.
@Value("#{concat(ord-, #{randomString(12)})}")
Supplier<String> idempotencyKey;

// Jittered retry delay (ISO 8601 duration with 100–500ms jitter).
@Value("PT0.#{randomInt(100, 500)}S")
Supplier<String> retryDelayIso;

// Live ISO timestamp on every call.
@Value("#{now()}")
Supplier<String> nowIso;

// Hot-reload config — picks up Settings.set(...) without bean re-creation.
@Value("${myapp.cache.policy:write-through}")
Supplier<String> cachePolicy;

See Simple Variable Language — the VarTemplate compiled-template API, which describes how VarResolver.resolveSupplier(...) pairs with @Value Supplier<String> fields for live-reload.

Spring @Value compatibility​

For consumers migrating from Spring Boot, org.springframework.beans.factory.annotation.Value is detected by fully-qualified name (JsrSupport.SPRING_VALUE) — juneau-commons doesn't declare a compile-time dependency on Spring. The same resolution chain applies, so the following two beans are functionally identical inside a Juneau bean store:

public class A {
@org.apache.juneau.commons.inject.Value("${jwt.cache.ttl:PT5M}")
Duration ttl;
}

public class B {
@org.springframework.beans.factory.annotation.Value("${jwt.cache.ttl:PT5M}")
Duration ttl;
}

Spring Boot bridge (juneau-rest-server-springboot)​

Inside a Spring Boot application, SpringBeanStore automatically installs a SpringEnvironmentPropertySource the first time it's constructed (typically by SpringRestServlet.createBeanStore(...)). This means application.yaml keys are reachable from a Juneau @Value:

# application.yaml
myapp:
title: My Awesome App
cache:
ttl: PT10M
@Rest
public class MyResource extends BasicSpringRestServlet {

@Value("${myapp.title:Untitled}")
private String title;

@Value("${myapp.cache.ttl:PT5M}")
private Duration cacheTtl;
}

The bridge is removed when SpringBeanStore.clear() is called — important for tests that build many transient Spring contexts in the same JVM.

Per-resource @Rest(config=...) integration​

When a REST resource declares @Rest(config="my.cfg"), the resolved Config is wired into the resource's BeanStore as a PropertySource bean named "rest.config". Any @Value("${some-key}") site on the resource bean (or on a request-scoped bean whose BeanStore parent-walks to the resource's) consults this scoped PropertySource between the Settings local/global overrides and the global Settings sources chain.

Resolution order for resource-scoped @Value sites​

  1. Settings.setLocal(...) / Settings.setGlobal(...) overrides — still win. The test override contract is preserved (overrides beat resource Configs).
  2. The resource's @Rest(config=...) Config(s) — via the per-BeanStore PropertySource bean.
  3. The process-wide Settings.get(name) chain — reverse-walks the registered sources from the resolution-order section above.
  4. The @Value default branch — the ${key:default} literal.

Concrete example​

api.cfg on the classpath / cwd:

api.key = secret-A
api.url = https://a.example.org/

[section]
nested = nested-value
@Rest(config="api.cfg")
public class MyResource extends BasicRestServlet {

@Value("${api.key}") // → "secret-A"
String apiKey;

@Value("${section/nested}") // → "nested-value"
String nested;

@Value("${unknown:fallback}") // → "fallback" (key absent, default used)
String fallback;

@Value("${java.home}") // → System property (falls through to Settings)
String javaHome;
}

Isolation guarantee​

Each RestContext carries its own BeanStore with its own "rest.config" bean. Resource A's @Value("${foo}") resolves against A.cfg, Resource B's @Value("${foo}") resolves against B.cfg — the two never see each other's Config. The same isolation extends to MockRestClient's static RestContext cache and to long-lived Spring Boot contexts because the registration lives on the per-resource BeanStore, not on the process-wide singleton.

Inheritance (child wins, parent fills gaps)​

When @Rest(config="parent.cfg") on ParentResource and @Rest(config="child.cfg") on ChildResource extends ParentResource declare different config names, the child's keys win on collision and the parent's keys fill the gaps:

# parent.cfg
parent.only = parent-value
shared = parent-shared
# child.cfg
child.only = child-value
shared = child-shared
@Rest(config="parent.cfg")
public class ParentResource extends BasicRestServlet {
@Value("${parent.only}") String parentOnly;
@Value("${shared}") String shared;
}

@Rest(config="child.cfg")
public class ChildResource extends ParentResource {}

A ChildResource instance sees child-value for child.only, parent-value for parent.only (child's Config doesn't define it — parent fills the gap), and child-shared for shared (child wins on collision).

setLocal / setGlobal still win​

The resource-scoped Config is consulted after the Settings override stores, so existing test fixtures that call Settings.get().setGlobal("api.key", "from-test") continue to take precedence over api.cfg — matching the established override contract.

Supplier<String> re-evaluates against the scoped BeanStore​

@Value Supplier<String> field types compose with the resource-scoped chain: the BeanStore reference is captured (by-reference, not snapshotted) by the supplier, so every .get() call re-evaluates against the current state of the resource's PropertySources. This means hot- reload of the resource Config (or Settings.set(...) of an override key) takes effect on the next .get() call without rebuilding the resource bean.

Classpath-default Config (juneau-config)​

The juneau-config module ships a META-INF/services entry for ConfigPropertySourceProvider. At startup, Settings.useServiceLoader() discovers the provider and registers a lazy bridge to Config.getSystemDefault(). The resolution is deferred to the first lookup so it can't trigger class-init cycles, and the bridge is silent when no classpath default juneau.cfg exists.

This means a plain juneau.cfg on the classpath transparently feeds @Value("${section/key}") without any explicit wiring.

Mutual exclusion with @Inject​

@Value and @Inject on the same field, parameter, or setter is an error:

@Inject
@Value("${some.key}") // BeanCreationException at injection time
private String value;

The exception message identifies the offending site to make the conflict easy to fix.

See also​