skip to content

How does ${...} placeholder resolution work against the PropertySources, and what role does PropertySourcesPlaceholderConfigurer play?

level: middleimportance: must knowfreq 52%

answer

  1. ${key:default} default after colon
  2. resolves via ordered PropertySources, first match
  3. PropertySourcesPlaceholderConfigurer = BeanFactoryPostProcessor, must be static @Bean
  4. Boot auto-registers it; plain Spring you declare it
  5. unresolvable → throws unless ignoreUnresolvablePlaceholders

basics

~10 s

${key} placeholders are resolved by looking key up in the Environment's ordered PropertySources. ${key:default} supplies a fallback. In plain Spring, a PropertySourcesPlaceholderConfigurer bean resolves these placeholders; Spring Boot registers one automatically.

solid answer

~40 s

A ${key} placeholder is resolved by consulting the Environment's PropertySources in order (first match wins). ${key:default} provides a default if the key is absent, and placeholders can nest (${outer.${inner}}). Resolution is driven by a PropertySourcesPlaceholderConfigurer — a BeanFactoryPostProcessor that resolves ${...} in bean definitions and @Value against the Environment. Spring Boot auto-registers it (PropertyPlaceholderAutoConfiguration); in plain @Configuration you declare it as a static @Bean so it runs early enough. Placeholders also work inside @PropertySource's own location, e.g. @PropertySource("classpath:${region}.properties"), resolved against sources already registered. By default an unresolvable placeholder with no default throws; you can set ignoreUnresolvablePlaceholders. The Environment also resolves placeholders directly via env.resolvePlaceholders("...").

code

java · 15 lines
java
@Configuration
@PropertySource("classpath:config-${env:dev}.properties") // placeholder in the location itself
public class AppConfig {

    // In PLAIN Spring you must register this; must be static.
    @Bean
    static PropertySourcesPlaceholderConfigurer placeholders() {
        var c = new PropertySourcesPlaceholderConfigurer();
        c.setIgnoreUnresolvablePlaceholders(false); // fail fast on typos
        return c;
    }
}

// Programmatic resolution needs no configurer:
// String url = env.resolveRequiredPlaceholders("jdbc://${db.host}:${db.port:5432}/app");

go deeper

for a junior

Know ${key} reads a property and ${key:default} gives a fallback.

for a middle

Explain that resolution goes through the ordered PropertySources and that a PropertySourcesPlaceholderConfigurer (auto in Boot) drives ${...} in @Value/bean defs.

for a senior

Cover static @Bean requirement, ignoreUnresolvablePlaceholders, nesting, placeholders in @PropertySource locations, and env.resolvePlaceholders.

for a principal

Weigh fail-fast vs ignore semantics for config safety, the legacy-configurer trap, and interaction with Boot's ConfigData/relaxed binding.

**What a placeholder is.** The `${...}` syntax is text substitution: `${db.url}` means "replace me with the value of property `db.url`". Resolution always goes through the Environment's ordered `PropertySources` (same first-match-wins lookup as `getProperty`). **Syntax features.** - **Default value:** `${db.port:5432}` → uses `5432` if `db.port` is undefined. The separator is `:`. - **Nesting:** `${db.${env}.url}` — the inner placeholder resolves first, then the outer. - **Escaping:** a literal `${` can be escaped as `\${` (via the placeholder prefix/escape handling in `PropertyPlaceholderHelper`). **Who does the resolving.** - `PropertySourcesPlaceholderConfigurer` is a `BeanFactoryPostProcessor` (`static`-registered) that, at container startup, resolves `${...}` in **bean definition metadata** and registers a `StringValueResolver` so `@Value("${...}")` resolves against the Environment. In **Spring Boot** it is auto-configured by `PropertyPlaceholderAutoConfiguration`, so you rarely declare it. In **plain @Configuration**, you must add it yourself, and it must be a **static** `@Bean` method (because BeanFactoryPostProcessors are instantiated very early, before regular bean configuration). - The **Environment itself** can resolve placeholders on demand via `environment.resolvePlaceholders("jdbc:${db.host}")` and `resolveRequiredPlaceholders(...)` — no configurer needed for programmatic use; the configurer is what wires it into `@Value` and XML/annotation bean definitions. (Note: how the *resolved* value is then bound/injected into a field via `@Value` or `@ConfigurationProperties` is the Value-Injection leaf's concern; here we care only about the ${...}→PropertySources resolution mechanism.) **Placeholders in @PropertySource locations.** `@PropertySource("classpath:config-${spring.profiles.active:dev}.properties")` is legal. The location is resolved **against sources already present** when that annotation is processed (system props, env, and earlier-processed @PropertySource files). You **cannot** reference a property defined by the *same* or a *later* @PropertySource — chicken-and-egg. **Unresolvable placeholders.** If `${x}` has no value and no default, the default configurer **throws** an `IllegalArgumentException` ("Could not resolve placeholder 'x'"). Set `setIgnoreUnresolvablePlaceholders(true)` to leave the raw `${x}` text in place instead — useful when a downstream tool substitutes it, dangerous if it hides real misconfiguration. **Legacy note.** `PropertyPlaceholderConfigurer` (no "Sources") is the older, pre-3.1 variant that does **not** consult the Environment's PropertySources. Always prefer `PropertySourcesPlaceholderConfigurer`. **Gotchas.** (1) In plain Spring, forgetting the configurer means `@Value("${x}")` injects the literal string `${x}` (or fails) — Boot users forget it's auto-provided. (2) The configurer must be **static**, else you get a warning that it won't post-process early beans. (3) `:` is both the default separator and legal in values — nest/escape carefully. (4) Placeholder location in @PropertySource can't self-reference.

  • In a plain (non-Boot) @Configuration app, @Value("${app.name}") injects the literal string "${app.name}". Why?
    No PropertySourcesPlaceholderConfigurer is registered, so nothing resolves ${...} in @Value. Add a static @Bean PropertySourcesPlaceholderConfigurer. Boot provides one automatically, which is why the problem only shows up in plain Spring.
  • What happens to ${db.port} if it isn't defined anywhere and has no default?
    The configurer throws IllegalArgumentException ('Could not resolve placeholder db.port') at startup — fail-fast — unless ignoreUnresolvablePlaceholders is true, in which case the raw ${db.port} text is left in.
  • Why must the PropertySourcesPlaceholderConfigurer @Bean method be static?
    It's a BeanFactoryPostProcessor, instantiated very early before the enclosing @Configuration is fully processed. A non-static method would force early instantiation of the config class and log a warning that the BFPP won't be applied to all beans.

saying these in an interview costs you the question

  • Thinking @Value/${...} always works even in plain Spring without a configurer
  • Using the old PropertyPlaceholderConfigurer (ignores PropertySources) instead of PropertySourcesPlaceholderConfigurer
  • Declaring the configurer as a non-static @Bean
  • Assuming an unresolved placeholder silently becomes null/empty (it throws by default)
  • Believing a @PropertySource location can reference a key defined by that same @PropertySource

context