Explain SystemEnvironmentPropertySource's relaxed name matching and the subtler @PropertySource gotchas (YAML, location placeholders, repeatable ordering, profiles).
answer
- db.url env-lookup also tries DB_URL (dots↔_, case)
- @PropertySource: no YAML without custom factory
- location placeholder can't self-reference
- repeatable: last-processed wins, still below env
- not profile-aware unlike Boot application-{profile}
basics
~10 sSystemEnvironmentPropertySource matches names leniently: env.getProperty("db.url") also finds DB_URL (dots↔underscores, case-insensitive). @PropertySource gotchas: no YAML by default, its location placeholder can't self-reference, later declarations override earlier ones, and it isn't profile-aware like Boot's config files.
solid answer
~40 sOS environment variables can't contain dots, so StandardEnvironment wraps them in a SystemEnvironmentPropertySource that does relaxed lookup: asking for db.url also checks DB_URL, db_url, and case variants — letting a canonical dotted property name be satisfied by an UPPER_SNAKE env var. Key @PropertySource gotchas: (1) the default PropertySourceFactory reads only .properties/.xml, not YAML — you need a custom factory. (2) Placeholders in the annotation's own location are resolved only against sources already registered, so it can't reference a property from the same or a later @PropertySource. (3) It's repeatable; the last-processed declaration outranks earlier ones but all sit below system/env. (4) @PropertySource is not profile-aware — unlike Boot's application-{profile} files — so profile switching needs a placeholder in the location or separate config. (5) ignoreResourceNotFound only guards a missing file, not a broken one.
code
java · 18 lines// Custom factory to make @PropertySource read YAML.
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
var factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
Properties props = factory.getObject();
String srcName = (name != null) ? name : resource.getResource().getFilename();
return new PropertiesPropertySource(srcName, props);
}
}
@Configuration
@PropertySource(value = "classpath:app.yml", factory = YamlPropertySourceFactory.class)
class Config { }
// Relaxed env lookup: with OS var DB_URL set, this returns it.
// env.getProperty("db.url") -> value of DB_URLgo deeper
Likely unaware of relaxed env matching or the YAML limitation; fine to know only basics.
Should know @PropertySource is .properties-only and that env vars can satisfy dotted names.
Explains SystemEnvironmentPropertySource transformations, the custom-factory YAML fix, and repeatable ordering.
Articulates canonical-name + UPPER_SNAKE env strategy, config-class processing order for location placeholders, profile handling in plain Spring, and where core relaxed matching ends and Boot relaxed binding begins.
**1. Relaxed env-var matching (SystemEnvironmentPropertySource).** POSIX environment variable names typically allow only letters, digits, underscores and can't hold `.` or `-`. Spring's canonical property names use dots (`db.url`). To bridge this, `StandardEnvironment` registers OS env vars as a `SystemEnvironmentPropertySource`, whose `getProperty`/`containsProperty` try several transformations of the requested name: - exact: `db.url` - dots→underscores: `db_url` - upper-case: `DB.URL` - dots→underscores + upper-case: `DB_URL` So `env.getProperty("db.url")` transparently finds an env var `DB_URL`. This is the *core* relaxed mapping. (Spring Boot's `@ConfigurationProperties` adds even broader **relaxed binding** — hyphens, camelCase, index syntax — but that richer binding is the Value-Injection/Boot layer, not the core Environment.) **2. @PropertySource does NOT load YAML.** The default `DefaultPropertySourceFactory` handles `.properties` and `.xml` (`Properties` format) only. `@PropertySource("classpath:app.yml")` silently loads it as a *properties* file and misparses it (or yields odd keys). To load YAML you must pass `factory = YamlPropertySourceFactory.class` where that custom factory wraps `YamlPropertiesFactoryBean`/`YamlPropertySourceLoader`. This is one of the most common surprises for people migrating from Boot's `application.yml` (which uses a different loader entirely). **3. Placeholders in the location are resolved against already-present sources.** `@PropertySource("classpath:app-${region:us}.properties")` works, but `${region}` must be resolvable from sources that exist **when this annotation is processed** — system properties, env vars, or an earlier-processed @PropertySource on an already-parsed config class. You **cannot** reference a key defined by this same file or a not-yet-processed one (chicken-and-egg). Processing order of config classes therefore matters. **4. Repeatable ordering.** `@PropertySource` is `@Repeatable` (container `@PropertySources`). Among multiple declarations, Spring inserts each new source **before** the previously added one, so the **last-declared/last-processed wins** — but *all* @PropertySource files sit **below** `systemProperties` and `systemEnvironment`. Two declarations with the same `name` merge into a `CompositePropertySource`. **5. Not profile-aware.** @PropertySource has no built-in `application-{profile}.properties` convention — that's Spring Boot's `ConfigData`. To switch files per profile in plain Spring you either put a placeholder in the location (`config-${spring.profiles.active}.properties`) or declare profile-scoped `@Configuration` classes each with their own @PropertySource behind `@Profile`. **6. ignoreResourceNotFound scope.** `ignoreResourceNotFound=true` only suppresses a *missing* resource; a present-but-malformed file still throws. And it's evaluated per-declaration. **7. Encoding.** `encoding="UTF-8"` matters for non-ASCII values; the default `.properties` reader is ISO-8859-1 for the classic format, so set encoding explicitly for UTF-8 files. **8. Immutability of the map behind sources.** `SystemEnvironmentPropertySource` reads the live `System.getenv()` map (read-only). You can't mutate env vars at runtime; to override, add a higher-precedence source (`addFirst`). **9. Case-sensitivity elsewhere.** Only the env-var source is relaxed. A plain `MapPropertySource` or a `.properties` file is **exact-match, case-sensitive** — `Db.Url` ≠ `db.url` there. Don't rely on relaxed matching outside the env source. **Design takeaways.** Prefer canonical dotted lowercase property names and let env vars satisfy them via UPPER_SNAKE (the 12-factor pattern). Use a custom `PropertySourceFactory` if you truly need YAML via @PropertySource, but in Boot just use `application.yml`. Rely on explicit ordering (addFirst/EnvironmentPostProcessor) rather than accidental precedence, and don't assume profile-awareness from @PropertySource.
- Why can env.getProperty("db.url") return the value of an OS variable named DB_URL, but a .properties file key DB_URL would NOT satisfy db.url?Only the OS-env source is a SystemEnvironmentPropertySource with relaxed matching (dots↔underscores, case-insensitive). A ResourcePropertySource/MapPropertySource does exact, case-sensitive key matching, so DB_URL and db.url are distinct keys there.
- A teammate points @PropertySource at an application.yml and values come out wrong. What's happening and how do you fix it?The default DefaultPropertySourceFactory parses it as a .properties file, mangling the YAML. Supply factory=YamlPropertySourceFactory.class (wrapping YamlPropertiesFactoryBean), or in Spring Boot just rely on application.yml loaded by ConfigData instead of @PropertySource.
- Can @PropertySource("classpath:${db.name}.properties") use a key defined inside that same file?No — the location placeholder is resolved against sources already registered when the annotation is processed. The file isn't loaded yet, so ${db.name} must come from system properties, env vars, or an earlier-processed @PropertySource.
saying these in an interview costs you the question
- Assuming @PropertySource reads YAML like Boot's application.yml
- Expecting relaxed DB_URL↔db.url matching for .properties/Map sources (only env source is relaxed)
- Thinking a @PropertySource location can reference a property from its own file
- Believing @PropertySource honors application-{profile} conventions
- Assuming ignoreResourceNotFound also swallows parse errors