Placeholders

A placeholder ${key} is replaced by the value of the property key; ${key:default} uses default when key is not set.

Why

A fact should be written once. The shop's URL is set in one place, and the API address is built from it. A value that differs per machine comes from an environment variable, with a default for the laptop. Placeholders do both, without code.

Syntax

Form Result
${shop.name} The value of shop.name. Fails where the value is needed when no source has it.
${shop.port:8080} 8080 when shop.port is not set.
${shop.motto:} An empty string when shop.motto is not set.
${a:${b:c}} A default can contain placeholders.
${shop.${region}.url} A key can contain placeholders.
\${not.a.placeholder} A literal ${not.a.placeholder}. In .properties, escape the backslash: \\${.
${random.int(10)}, ${random.uuid} A new random value on every read.

Rules:

Example

# lib/resources/application.yml
shop:
  name: NEON Books
  title: ${shop.name} (${spring.profiles.active:local})
  url: ${SHOP_URL:http://localhost:8080}
  api: ${shop.url}/api/v${shop.api-version:1}
  instance-id: ${random.uuid}
@Service()
class ApiClient {
  ApiClient(
    // A raw string: in '${...}' Dart would interpolate.
    @Value(r'${shop.api}') this.baseUrl,
    @Value(r'${shop.timeout:5s}') this.timeout,
  );

  final String baseUrl;
  final Duration timeout;
}

Where they are resolved

Place When a placeholder cannot be resolved
Values in config files and other sources, when read through the environment Fails where the value is read
@Value Fails the start, naming the key. A nullable target gets null.
environment.getProperty, getPropertyAs Throws PlaceholderResolutionException
environment.resolvePlaceholders(text) Stays as written
environment.resolveRequiredPlaceholders(text) Throws PlaceholderResolutionException
@ConfigurationProperties fields Kept as written. @RawValue() skips resolution for one field.
@PropertySource locations Fails the start. With ignoreResourceNotFound: true the location is skipped.
spring.config.import locations Fails the start. An optional: import is skipped.
Starter annotations that take configurable text, such as @Scheduled(fixedDelayString: ...), the URL of a Feign client and request mapping paths Depends on the starter
EmbeddedValueResolverAware beans The bean calls the resolver itself

Placeholders are not SpEL. #{...} is evaluated only in @Value and @ConditionalOnExpression, never in config files. In @Value, placeholders are replaced first, so #{${shop.max-basket-size} * 2} works.

In Dart code, always write placeholders as raw strings: @Value(r'${shop.name}'). The dart_spring editor extension marks a plain string as an error.

Resolving your own text

PropertyPlaceholderHelper is the engine behind the environment. Use it to fill placeholders from something that is not the configuration, such as template variables:

final helper = PropertyPlaceholderHelper();
final values = {'host': 'localhost', 'port': '8080'};
helper.replacePlaceholders(
  r'http://${host}:${port:80}/${path:}',
  (key) => values[key],
); // 'http://localhost:8080/'

In Spring (Java)

@Value("${shop.api}") String baseUrl;
@Value("${shop.timeout:5s}") Duration timeout;

The syntax and rules are Spring's, including the Spring 6.2 lookup of ${db:url}. Differences that matter:

See also