Configuration in Full: Properties, YAML, Binding and Profiles

bee2026-10-0844 min read0 views
Config file precedence, loose binding and validation in @ConfigurationProperties, profile switching, encryption and externalization — every configuration question answered.
1 / 124
Section
0. The 30-second version
2 / 124

No Spring Boot magic in this article — just a plain @Component and a handful of annotations — so that you can answer one question confidently: where does a configuration value come from, and why did my file edit do nothing?

3 / 124

When a Spring Boot app starts it reads configuration from many places at once: the application.yml inside the jar, a second copy you dropped next to the jar, environment variables on the server, -D flags, and finally --server.port=9090 typed at the end of the command line. When the same key (say server.port) appears in several of them, Spring Boot does not complain and does not "merge" anything — it asks a fixed list of sources in order, and the first one that answers wins. This article covers four things: where config can live, which layer shadows which, how to get a value into a Java field (binding), and how Profiles let one codebase carry a different value set per environment.

4 / 124
Analogy

configuration precedence is a chain of authority. Your household rules (the application.yml inside the jar) govern daily life; company policy (an external config/ file and environment variables on the server) outranks the household; national law (command-line arguments) outranks everyone. For a rule as ordinary as "what time do you get home", the more specific and the closer to the present moment, the more it wins — which is why --server.port=9090 on the command line makes every file edit irrelevant: law beats household rules.

5 / 124
Analogy

Profiles are one apartment with three sets of furniture. The bare shell (application.yml) is the fixed skeleton — wiring and layout never move; home mode (application-dev.yml) adds a rug and warm lights; rental mode (application-prod.yml) swaps in durable furniture and locks the storage room. The structure is identical; only "the one set of values for this mode" changes. Activating prod simply switches the apartment into its rental arrangement.

6 / 124

After this article you should be able to answer three questions:

7 / 124
  • I edited the application.yml sitting next to the jar — why does the service still come up on the old port? Which layer shadows it?
  • @Value("${bee.max-size}") and @ConfigurationProperties(prefix = "bee") — does the same environment variable work for both? Why do the answers differ?
  • If spring.profiles.active is off by one letter in production, what happens, and how do I prove for myself that the profile really took effect?
8 / 124
Section
1. Three config carriers: properties, yml, yaml
9 / 124

Spring Boot lets you carry configuration in three file types, all of which are ultimately parsed into the same PropertySource. Choosing among them is mostly a team habit, but the differences are real:

10 / 124
Table
CarrierReadabilityHierarchyListsBest for
application.propertiesFlat; long lists read wella.b.c= dot notation gets verbose when deeplist[0]=x is awkwardSimple, small configs
application.ymlIndentation shows structureNative tree- x is obviousThe mainstream choice
application.yamlIdentical to ymlSameSameOnly the file name differs
11 / 124

The same config in two styles:

12 / 124
properties
# application.propertiessms.endpoint=https://sms.example.com/apisms.sign-name=Acme Inc.sms.retry.attempts=3sms.retry.enabled=true
13 / 124
Code
Codeyaml
# application.ymlsms:  endpoint: https://sms.example.com/api  sign-name: Acme Inc.  retry:    attempts: 3    enabled: true
Notes
  • yml expresses hierarchy with indentation, so you see at a glance which keys belong to which prefix; properties joins keys with dots, which gets hard to read as the depth grows
  • yml expresses lists more naturally: hosts:\n - a\n - b, versus the clunky hosts[0]=a in properties

Trap: YAML indentation allows spaces only, never tabs. Copying config from a web page is the classic way to smuggle a Tab character in, and startup fails with mapping values are not allowed here or found character '\t' that cannot start any token — errors that never mention "tab", leaving you baffled. Remember too: a colon must be followed by a space (key: value); key:value is treated as a plain string.

14 / 124
Section
2. Load order: which config wins when the key repeats
15 / 124

This is the table most worth internalizing. Spring Boot gathers configuration from many places, and when the same key appears in several, the highest-priority layer wins (note: not "the later overrides the earlier" but "the first match by priority is taken"):

16 / 124
Diagram
Figure 1 · Configuration precedence
Figure 1 · Configuration precedence
17 / 124
Table
PrioritySourceNotes
1Command-line argumentsjava -jar app.jar --server.port=9090, the highest
2SPRING_APPLICATION_JSONA JSON blob placed in an environment variable
3ServletConfig / ServletContext init paramsContainer level
4JNDI (java:comp/env)Traditional app servers
5Java system propertiesSystem.getProperties(), i.e. -Dkey=value
6OS environment variablesSERVER_PORT=9090
7random.*Random value source
8External profile-specific file under config/./config/application-prod.yml
9External profile-specific file./application-prod.yml
10Packaged profile-specific fileclasspath:/application-prod.yml
11External generic file under config/./config/application.yml
12External generic file./application.yml
13Packaged generic fileclasspath:/application.yml
14Default propertiesSpringApplication.setDefaultProperties(...)
18 / 124
  • Command line > environment variables > external files > packaged files > defaults is the memory spine
  • "External" means the directory holding the jar or a config/ beneath it, used to override config without repackaging
  • Profile-specific files (application-prod.yml) outrank generic files (application.yml), and both come after the packaged/external split
19 / 124
Tip

this explains a constant ops question — "I edited application.yml inside the container, why did nothing change?" If the start command carried --server.port=8081, that command-line layer always wins and the file edit is moot. The first step in troubleshooting config is always to ask "is it shadowed by a higher-priority layer?"

20 / 124

Fourteen rows is a lot to hold in your head, but a real investigation only ever walks three or four of them. Here is the same chain as boxes you can click through; box ③ carries the only idea in this section worth memorising: it is not merging, it is being asked first.

21 / 124
Diagram
FlowThe precedence chain, asked one box at a time1 / 6
Click ① to ⑥. A repeated key has exactly one winner, and the rule is 'asked first', not 'written last'
→
→
→
→
→
① Command-line arguments
`java -jar app.jar --server.port=9090` is set when the process starts, sits at the front of the queue, and is the thing everyone forgets. K8s args, a start script, a CI override — all of them are this layer. When 'my file edit did nothing', open the start command first.
All clearIn one line: precedence is 'who gets asked first', and a repeated key resolves to the first answer only.
22 / 124
Animation
Animation · How configs are located
Animation · How configs are located
23 / 124
Section
3. @Value vs @ConfigurationProperties: which one
24 / 124

Both read config into Java, but they differ deeply:

25 / 124
Table
Aspect@Value@ConfigurationProperties
Bindingexact single-key matchloose binding, batch binding of a whole prefix
Complex typesneeds hand-written SpELnative List / Map / nested objects / Duration
Validationnonesupported via @Validated
SpELsupported (it is an expression)not supported (use @Value if you need SpEL)
Refreshrefreshable with @RefreshScopeneeds a refresh mechanism
Best forstray single values (@Value("${app.name}"))grouped business config (strongly recommended)
26 / 124
java
// Case one: just grabbing one stray value — @Value is more direct@Componentpublic class HealthController {    @Value("${app.version:unknown}")   // text after the colon is the fallback    private String version;}
27 / 124
Code
Codejava
// Case two: grouped config — always @ConfigurationProperties@Component@ConfigurationProperties(prefix = "sms")public class SmsProperties {    private String endpoint;    private int timeout = 3000;    private Retry retry = new Retry();    public static class Retry {        private boolean enabled;        private int attempts;        // getters / setters    }}
Notes
  • @Value("${app.version:unknown}"): text after the colon is the default; but once a key is misspelled only runtime reveals it, and loose binding is not supported
  • @ConfigurationProperties: binds a whole prefix at once, with IDE hints, validation and testability — the default choice in engineering
  • The criterion is simple: one value uses @Value, a group of values uses @ConfigurationProperties
28 / 124
Section
4. Loose binding in full
29 / 124

"Relaxed binding" means a property's written form can vary while still binding to the same Java field. This reconciles three naming habits — kebab-case in yml, underscores in constants, camelCase in fields:

30 / 124
Diagram
Figure 2 · @Value and @ConfigurationProperties take different paths
Figure 2 · @Value and @ConfigurationProperties take different paths
31 / 124

The comparison that diagram is making: @Value("${sms.template-id}") performs a literal exact match — write sms.templateId and it refuses; whereas @ConfigurationProperties(prefix = "sms") compares normalized names, stripping separators and lower-casing both the property name and the field name, so all five forms below land on the same field:

32 / 124
Table
Written formBinds to field templateId?Typical scenario
sms.template-id✅yml (recommended)
sms.templateId✅properties
sms.templateid✅case is irrelevant
SMS_TEMPLATE_ID✅environment variables (best)
sms.template_id✅a slip in properties
33 / 124
Code
Codejava
@ConfigurationProperties(prefix = "sms")public class SmsProperties {    private String templateId;   // all five forms above bind to it}
Notes
  • The prefix prefix = "sms" also matches in relaxed form, so SMS_ENDPOINT maps to endpoint under sms
  • The recommended environment-variable form is all-caps with underscores: SMS_ACCESS_KEY corresponds to sms.access-key
  • One detail: environment variables match fine in all caps on Linux, but to bind a kebab-case key, underscores are safest — SMS_ACCESS_KEY is translated to sms.access-key automatically

Key point: asked "why do environment variables and yml bind despite different spelling", the answer is that @ConfigurationProperties normalizes both the property name and the field name into "separators removed, lower-cased" before comparing. Understand this and you stop agonizing over "template-id or templateId" — kebab-case in yml, underscores in environment variables, both work.

34 / 124
Section
5. Complex type binding: List, Map, nested objects, Duration, DataSize
35 / 124

The real power of @ConfigurationProperties is complex types, which @Value can hardly do:

36 / 124
yaml
# application.ymlapp:  name: Demo  hosts:                 # List<String>    - 10.0.0.1    - 10.0.0.2  channels:              # Map<String, Integer>: scalar keys and values    wechat: 1    sms: 2  datasource:            # nested object    url: jdbc:mysql://localhost:3306/demo    pool:      max-size: 20      min-idle: 5  timeouts:    connect: 2s          # Duration    read: 500ms  upload:    max-file-size: 10MB  # DataSize
37 / 124
Code
Codejava
@ConfigurationProperties(prefix = "app")public class AppProperties {    private String name;    private List<String> hosts;    private Map<String, Integer> channels;    private DataSource dataSource = new DataSource();    private Timeouts timeouts = new Timeouts();    public static class DataSource {        private String url;        private Pool pool = new Pool();        public static class Pool {            private int maxSize;            private int minIdle;        }    }    public static class Timeouts {        private Duration connect;   // "2s" / "500ms" / "PT2S" all parse        private Duration read;    }}
Notes
  • List<String>: a - list in yml, hosts[0]= with an index in properties
  • Map<String, Integer>: listed directly as "key: value" pairs in yml
  • DataSize: parses 10MB, 1GB and friends into org.springframework.util.unit.DataSize
  • Duration: parses 2s, 500ms, PT2S; without a unit it is treated as milliseconds, so always include one

Trap: Duration and DataSize behave differently when unitless — read: 500 is taken as milliseconds, while max-file-size: 10 defaults to bytes. These "number without a unit" configs break most easily across environments; the convention is to always carry a unit, which is both safer and self-documenting.

38 / 124
Section
6. Validation: @Validated is the key
39 / 124

When config is wrong, the ideal is to fail fast at startup and name the offending field. That takes a trio: @Validated + validation annotations + a validation implementation:

40 / 124
java
@Validated                                   // 1) mandatory, or validation never runs@ConfigurationProperties(prefix = "sms")public class SmsProperties {    @NotBlank                                // 2) non-null and not blank    private String endpoint;    @Min(500) @Max(10000)    private int timeout = 3000;    @Email    private String alertEmail;}
41 / 124
xml
<!-- 3) add a validation implementation (explicit since Spring Boot 2.3) --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-validation</artifactId></dependency>
42 / 124

When config is invalid, startup fails with a message pinpointing the field:

43 / 124
Code
Codetext
Failed to bind properties under 'sms' to com.example.sms.autoconfigure.SmsProperties:    Property: sms.endpoint    Value: ""    Origin: class path resource [application.yml] - 3:13    Reason: must not be blank
Notes
  • @Validated is the switch: without it, @NotBlank / @Min on fields simply do not run
  • The implementation (hibernate-validator) has not been included by default since Spring Boot 2.3, so you must add spring-boot-starter-validation
  • The Property / Value / Origin (which file, which line) in the error are gold — they locate the exact source

Tip: treat validation as the contract of your configuration. That endpoint is non-blank, that timeout lies in a sane range — write these constraints into the properties class and you set rules for the whole team: bad config is stopped at startup rather than discovered when messages fail at midnight.

44 / 124

Once the trio is in place, generating the properties class beats copying one — and pay particular attention to the last option: the same class can be admitted three different ways, and each emits different code.

45 / 124
Generator
GeneratorA properties class that can defend itselfProperties class + yml3 / 7
Tick only 'Validation' and 'Emit the matching application.yml' first, and watch @Validated pair with kebab-case field names. Then add 'Nested object' and 'Duration / DataSize' and compare with the two unitless traps in Section 5. Finally switch 'Emit how to register it' to see @EnableConfigurationProperties against @ConfigurationPropertiesScan — the beanin lab in Section 11 walks exactly those four doors
Output
package com.example.sms;

import java.time.Duration;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

/** 配置前缀 sms:所有 sms.* 的键都绑进这个类 */
@ConfigurationProperties(prefix = "sms")
@Validated
public class SmsProperties {

    /** 必填,空值启动即失败 */
    @NotEmpty
    private String name;

    /** 配置里可以写 30s / 5m */
    private Duration timeout = Duration.ofSeconds(3);

    private String accessKeySecret;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getAccessKeySecret() { return accessKeySecret; }
    public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; }

    public Duration getTimeout() { return timeout; }
    public void setTimeout(Duration timeout) { this.timeout = timeout; }

}

# ---------------- 配套 application.yml ----------------
sms:
  name: bee-order                 # 短横线/驼峰都能绑,推荐统一写短横线
  timeout: 30s
  access-key-secret: ${OSS_SECRET:}   # 密钥从环境变量进来,别写进仓库
Why each choice matters
加校验Failing at startup beats discovering timeout=0 at noon on day one.
Duration / DataSizeDuration beats int: config can read 30s / 5m and a wrong unit fails at startup.
生成配套 application.yml 片段Field names go kebab-case: accessKeySecret → access-key-secret — that is relaxed binding.
松散绑定One property accepts access-key-secret / accessKeySecret / ACCESS_KEY_SECRET / access_key_secret; when two spellings appear in the same file the weaker one gets overwritten — standardise on kebab-case.
46 / 124
Section
7. Profiles in full: one codebase, many environments
47 / 124

Profiles are Spring's official answer to multiple environments: one codebase that loads different config and wires different beans per active profile.

48 / 124

First, the @Profile annotation — essentially a preset of conditional wiring:

49 / 124
java
@Configuration@Profile("dev")public class DevMailConfig {    @Bean    public MailSender mailSender() {        return new ConsoleMailSender();   // dev: mail is only printed to the console    }}
50 / 124
java
@Configuration@Profile("prod")public class ProdMailConfig {    @Bean    public MailSender mailSender() {        return new SmtpMailSender();      // prod: real SMTP sending    }}
51 / 124

A far more common approach uses profile-specific config files, folding the differences into files instead of code:

52 / 124
yaml
# application.yml — shared configspring:  application:    name: demosms:  sign-name: Acme Inc.
53 / 124
yaml
# application-dev.yml — developmentspring:  datasource:    url: jdbc:h2:mem:demo    username: sa    password: ""logging:  level:    com.example: debug
54 / 124
yaml
# application-prod.yml — productionspring:  datasource:    url: jdbc:mysql://db.internal:3306/demo    username: demo    password: ${DB_PASSWORD}     # injected from an environment variable, never hard-codedlogging:  level:    com.example: info
55 / 124

spring.profiles.active has four common ways to activate:

56 / 124
Table
WaySyntaxBest for
Config filespring.profiles.active: proda safe default
Command-line arg--spring.profiles.active=prodtemporary override (highest priority)
Environment variableSPRING_PROFILES_ACTIVE=prodcontainers / CI (recommended)
CodeSpringApplication.setAdditionalProfiles("prod")programmatic setup
57 / 124

Profile expressions also support logic:

58 / 124
Code
Codeyaml
# enabled in every environment except devspring:  profiles:    active: "!dev"
Notes
  • @Profile("dev"): the configuration class is active only when dev is active; multiple profiles use an array @Profile({"dev", "test"}) (any match wins)
  • Negation and combination: @Profile("!prod") means "not prod", @Profile({"prod", "cloud"}) is an "or"; complex combinations use expressions
  • Profile-specific files are loaded automatically with the active profile, no manual import needed
59 / 124

"Activate prod" fires two things at once: one file joins the configuration chain, and a batch of bean conditions turn green. This animation runs both lines together — look at frame 6, because @Profile and the profile-specific file are driven by one and the same switch:

60 / 124
Animation
Animation · What activating a profile really triggers
Animation · What activating a profile really triggers
61 / 124
Trap

when yml and properties coexist, both load and properties wins. If a project has both application.yml and application.properties, the same key resolves to .properties — many "I changed yml and nothing happened" cases start here, with an old .properties still lying around. Profile file names are also case-sensitive: application-Prod.yml does not match prod.

62 / 124

Which lines actually belong in a multi-environment setup is faster to generate than to copy. Tick "per-env blocks" alone first and watch how --- pairs with spring.config.activate.on-profile; then add datasource and logging and compare with the application-dev.yml / application-prod.yml split above; finally add the server block and see why server.port so often loses to an environment variable in production (lab prop order in Section 11 replays that chain).

63 / 124
Generator
GeneratorConfigure the multi-environment lines in one passapplication.yml2 / 5
Tick in this order — per-env blocks, then datasource, then logging, then server. Each time you add one, decide whether it belongs in the generic file or in the profile-specific one; the whole division of Section 7 is in the output
Output
server:
  port: 8080

spring:
  application:
    name: demo-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/bee_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
    username: ${DB_USER:root}          # ${} 占位符:环境变量优先,冒号后是默认值
    password: ${DB_PASS:}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      max-lifetime: 1740000            # 必须小于 MySQL 的 wait_timeout
      pool-name: beeHikari

---
spring:
  config:
    activate:
      on-profile: prod
logging:
  level: { root: WARN }
---
spring:
  config:
    activate:
      on-profile: dev
spring:
  jpa:
    show-sql: true
Why each choice matters
datasourcePool settings only apply here; constructing HikariDataSource in code ignores every one of them.
profiles + 分档配置Multi-document blocks are split by --- and activated with spring.config.activate.on-profile.
64 / 124
Section
8. Encrypting config and handling secrets
65 / 124

Production database passwords and keys must never sit in plaintext in the artifact. There are two mainstream routes:

66 / 124
  • Jasypt in one line: add jasypt-spring-boot-starter, write ENC(ciphertext) in config, and decrypt at startup with a key. Fits "the ciphertext must live in the config file" situations
  • Environment-variable injection (preferred): write only a placeholder ${DB_PASSWORD} in config and let the platform (K8s Secret, CI variable) inject the real value — the password never appears in the repository at all
67 / 124
yaml
spring:  datasource:    password: ${DB_PASSWORD}   # injected at deploy time; the repo sees no real value
68 / 124

Writing ${DB_PASSWORD} like that is placeholder resolution: the container substitutes the real value during the binding phase, and the substituted result then goes through the precedence chain again — which means the name DB_PASSWORD itself competes on that chain.

69 / 124
Animation
Animation · How ${DB_PASSWORD} becomes a real value
Animation · How ${DB_PASSWORD} becomes a real value
70 / 124

Three rules govern resolution, and knowing them means never being stumped by "why can't my yml placeholder see the environment variable":

71 / 124
  • The name is looked up on the precedence chain first. If DB_PASSWORD happens to be defined in some application.yml as well, that value wins — not the OS environment variable
  • A colon supplies a fallback. ${DB_PASSWORD:guest} yields guest when nothing is found and never fails; ${DB_PASSWORD} throws instead
  • Not found means an exception that names the key. The full text is org.springframework.beans.factory.BeanCreationException: Could not resolve placeholder 'DB_PASSWORD' in value "${DB_PASSWORD}" — searching the first half finds it
72 / 124
Trap

a placeholder cannot reference itself into existence. Nesting like password: ${DB_PASSWORD:${DB_PASSWORD:123456}} is syntactically legal, but the moment the first level resolves the error message can collapse into an unreadable chain of ${...}. At most one level of fallback, and write the fallback value out explicitly.

73 / 124
Key point

prefer environment variables over config encryption whenever you can. Encryption merely swaps "plaintext" for "ciphertext that needs a key", and the key itself still has to live somewhere; environment-variable injection takes secrets out of the repository entirely and pairs cleanly with K8s Secret or Vault. Jasypt suits older systems whose deployment pipeline cannot be changed.

74 / 124
Key point

resolution happens before binding, so a placeholder can pull from an environment variable (${DB_PASSWORD}), from another config key (${server.port}), even from a system property. Precisely because of that, password: ${DB_PASSWORD} and password: ${spring.datasource.password} in the same yml file behave identically — yet when debugging, the former sends you to the environment while the latter sends you back inside yml to trace the reference chain.

75 / 124

"Before binding" is what gives these failures their particular shape, so unfold it as a single-step run: six lines of startup on the left, variables and call stack refreshing on the right. Step through and stop on beat ④ — that beat asks for a property name, not for an operating-system variable name:

76 / 124
Stepper
StepperStep by step: how ${DB_PASSWORD:guest} gets answered1 / 6
Six beats. Watch 'which source is being asked' and 'final value'; beat 6 exists only because beat 4 failed to hit
Code under debug
1// this line in application.yml: password: ${DB_PASSWORD:guest}
2// the first thing SpringApplication.run does: order six sources into the Environment
3// PropertySourcesPlaceholderConfigurer walks every ${...} it can find
4// down the queue it asks each source: do you hold a property named DB_PASSWORD
5// hit → substitute; nothing found → take guest after the colon
6// only the substituted string then goes to the Binder for relaxed binding and conversion
Variables now
the raw linepassword: ${DB_PASSWORD:guest}
what it is right nowone string
who has read itnobody
Call stack
1classpath:/application.yml
1At this moment that line is nothing at all — the half after the colon is merely part of a string. A placeholder is resolved, not read, and that single fact explains why these errors surface far earlier than people expect.
77 / 124

Beat ④ above is the single most frequent production-only startup failure in this chapter. Here is the real stack — do not read the analysis, just click the frame you think is guilty:

78 / 124
Triage
Error triageIllegalArgumentException: Could not resolve placeholder 'DB_PASSWORD'
Fine on the laptop, dead on prod: a placeholder whose name nobody owns

The IDE run configuration carried DB_PASSWORD, so development worked. On K8s the Secret injects SPRING_DATASOURCE_PASSWORD, startup fails in the first second, and the stack contains not one business class.

APPLICATION FAILED TO START
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'smsClient' defined in class path resource [com/example/sms/autoconfigure/SmsAutoConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not resolve placeholder 'DB_PASSWORD' in value "${DB_PASSWORD}"
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600)
at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.processProperties(PropertySourcesPlaceholderConfigurer.java:188)
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:180)
at com.example.notice.NoticeService.<init>(NoticeService.java:21)
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'DB_PASSWORD' in value "${DB_PASSWORD}"
Environment in use (6 property sources, highest first):
commandLineArgs, systemProperties, systemEnvironment, config/application-prod.yml, classpath:/application.yml, classpath:/application.properties
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
79 / 124
Section
9. @RefreshScope and dynamic refresh (a preview)
80 / 124

Everything above is "read once at startup, then fixed". In Spring Cloud there is sometimes a need to update config without a restart — that is where @RefreshScope comes in: it wraps the bean in a proxy, destroys and rebuilds it when config changes, so new values are read.

81 / 124
Code
Codejava
@RefreshScope@Component@ConfigurationProperties(prefix = "sms")public class SmsProperties {    // after a config-center push, this picks up the new value}
Notes
  • @RefreshScope makes the bean a lazily created proxy, rebuilt only on refresh
  • It belongs to Spring Cloud and is rarely needed in a monolith; for now just know that "dynamic config refresh relies on it", covered later in the Spring Cloud series
82 / 124
Section
10. Two high-frequency traps
83 / 124
Table
SymptomRoot causeFix
Edited yml has no effectAn application.properties sits alongside it and winsDelete it or standardize on one carrier
Profile does not switchWrong file case (application-Prod.yml) / wrong activationUse lowercase application-prod.yml, verify spring.profiles.active
84 / 124
Trap

when yml and properties coexist, both load and .properties wins. This is not "merged into one" but two independent PropertySources ordered by priority, with properties first. The first step in cleaning up config is to search the whole project for how many application.* files exist.

85 / 124
Trap

profile file names are case-sensitive. application-dev.yml matches dev, application-Dev.yml does not; and spring.profiles.active=Dev is a different profile from dev. The convention is always lowercase, eliminating the possibility of a capital letter altogether.

86 / 124

Configuration errors share one temperament: the message never names the actual cause. mapping values are not allowed here never says "tab", Could not resolve placeholder never says "the names do not match". So rather than memorising, click once — exact wording on the left, what it is really complaining about on the right:

87 / 124
Match
MatchError text paired with its real causeMatched 0/6 · Missed 0
Every left-hand phrase can be pasted verbatim into a search box; the right column is what it actually points at, and a wrong pick explains itself
Pick a card on the left first
88 / 124
Section
11. Hands-on: whether config is read changes the bean
89 / 124

This chapter has nine kernel labs, and one pass in this order is enough: ① first see how "config read or not" changes the bean, then ②③④⑤ press precedence, relaxed binding, placeholders and profiles individually, ⑥ then climb up to the startup spine to find where configuration is already in place, and finally ⑦⑧⑨ reconnect it to the three underlying mechanisms — conditional wiring, how beans get admitted, and auto-configuration.

90 / 124

The first demo turns "config has been read" into a switch. Toggle it and watch the DataSource pool parameters (max connections, timeout) change with the config:

91 / 124
Kernel lab
92 / 124

The second lab is the direct cure for "I edited the config and nothing happened": write the same server.port into the command line, an environment variable and a packaged file, then watch the precedence chain ask each one in turn.

93 / 124
Kernel lab
TeaVMSame key, five places: the precedence chain liveidle
Switch to order and compare one key across the command line, the environment and the packaged file
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
94 / 124

The third lab turns the Section 4 table into something clickable: switch the written form of a property and see whether it still binds to the templateId field.

95 / 124
Kernel lab
TeaVMRelaxed binding: five spellings, one fieldidle
Switch to relaxed and try sms.template-id, sms.templateId and SMS_TEMPLATE_ID one by one
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
96 / 124

The fourth lab shows placeholder resolution in order, including the fallback and the failure case.

97 / 124
Kernel lab
TeaVMPlaceholder resolution: how ${DB_PASSWORD} gets replacedidle
Switch to ph and watch the three endings: a value present, no value, a fallback written
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
98 / 124

The fifth lab plays out Section 7's "one letter off" trap: the profile name has to match in two places, the activation setting and the file name. Miss either and the app falls back to the generic config — and still starts happily.

99 / 124
Kernel lab
TeaVMProfile activation: four ways to set it, two places the name must matchidle
Switch to profile and try the command line, the environment variable, the yml and the programmatic form one by one; then watch application-Prod.yml quietly do nothing
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
100 / 124

The sixth lab asks the question that comes earlier: at which step of startup is configuration actually read? The answer is before the container refreshes, and that is why Section 8's "resolution before binding" and Section 13's "the value you see in @PostConstruct is already final" are both the same fact:

101 / 124
Kernel lab
TeaVMWhere in the eight-step startup does the Environment get readyidle
Walk the eight steps and watch the order of prepareEnvironment versus refreshContext; then switch to fail to see what 'config never arrived' looks like in the startup log
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
102 / 124

The seventh lab puts @Profile back where it belongs — in conditional wiring. A configuration class that loses does not error, it just leaves a verdict line in the report, exactly like article 19:

103 / 124
Kernel lab
TeaVM@Profile is a condition too: watch it lose inside the reportidle
See whether DevMailConfig or ProdMailConfig lands under Positive matches, and which condition class the verdict line names
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
104 / 124

The eighth lab untangles the two "no such bean" messages of this chapter: a properties class that declared itself but was never admitted, versus one that is not even in scan range. The wording is nearly identical:

105 / 124
Kernel lab
TeaVMWhich door does a properties class use: @Component, @EnableConfigurationProperties or a scanidle
Run scan / bean / import / auto once each and compare when each door writes its definition, then use miss to see the failure case
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
106 / 124

The ninth lab answers "why does @Value work with nothing configured at all": the bean that resolves placeholders, PropertySourcesPlaceholderConfigurer, is itself the product of one auto-configuration, and here is which questions it had to pass:

107 / 124
Kernel lab
TeaVMBehind @Value working out of the box: the candidate list and its filter stationidle
Look at where candidates come from first, then at filter, and match it against the four-ways-to-read table in Section 13
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
108 / 124

Enough buttons — type the commands yourself. This console talks to the same in-browser kernel, and every reply is computed there:

109 / 124
Console
110 / 124
Note

cond configLoaded false means nothing until you also type restart — the switch only changes configuration, it does not refresh the Environment for you. That little sequence is the live version of the comparison table in Section 10.

111 / 124
Section
Sandbox: one key, five sources, who wins
112 / 124
Sandbox
SandboxOne key, five sources, who wins
Result
final server.port = 8080
why: the packaged generic file is the only layer holding a value
which is the default written in the application.yml of this very project
With nothing of higher priority around, the value comes from the packaged application.yml. Most "the port mysteriously became 8080" stories end exactly here.
113 / 124
Section
Quick quizzes
114 / 124
Quiz
Check yourself`application.yml` says `server.port: 8080` and the start command is `java -jar app.jar --server.port=9090`. Which port does the service actually listen on?
Pick one — you get feedback right away
115 / 124
Quiz
Check yourself`@Value("${sms.template-id}")` finds nothing, yet with the same setup the `templateId` field on the properties class binds fine once the key is spelled `SMS_TEMPLATE_ID` as an environment variable. Where is the difference?
Pick one — you get feedback right away
116 / 124
Section
12. Decision: where should the production DB password live
117 / 124
Decision
Decisionwhere should the production database password go?
118 / 124
Section
13. Four ways to read a value (and one trap that fools everyone)
119 / 124

Earlier sections covered @Value and @ConfigurationProperties, but engineering actually has four roads to a value. The trade-off sits on two axes: type safety (do you get an int/Duration or a string you convert yourself) and granularity (one key at a time, or a whole prefix at once).

120 / 124
Diagram
Figure · The four ways to read config, as a quadrant
Figure · The four ways to read config, as a quadrant
121 / 124
Table
WayHow it is writtenRelaxed bindingGranularityValidationWhen to use it
@Value("${sms.endpoint}")field annotation❌ literal match onlyone key❌one or two scattered values; SpEL needed
@ConfigurationProperties(prefix = "sms")properties class✅whole prefix✅ with @Validatedthe default for grouped business config
Environment env; env.getProperty("sms.endpoint")inject the container object, read by hand❌any key, decided at runtime❌the key name is only known at runtime, or you must enumerate sources
System.getProperty("...") / System.getenv("...")plain JDK, bypasses Spring❌none❌only before the framework initialises (logging config, etc.)
122 / 124

The third row is the one beginners never discover, because nobody shows them the code:

123 / 124
Code
Codejava
@Componentpublic class ConfigProbe {    private final Environment env;    public ConfigProbe(Environment env) {   // Environment is the master switch for all configuration        this.env = env;    }    @PostConstruct    public void report() {        // 1) one value: asked around the sources, the first hit wins        System.out.println("server.port = " + env.getProperty("server.port"));        // 2) prove whether the profile actually took effect — the first line of config debugging        System.out.println("active profiles = " + Arrays.toString(env.getActiveProfiles()));        // 3) how many configuration layers exist, and in which order        ((AbstractEnvironment) env).getPropertySources()                .forEach(ps -> System.out.println("source: " + ps.getName()));    }}
Notes

Trap: System.getProperty("server.port") most likely returns null in Spring Boot. It reads the JVM’s own system-property table, which only receives values written as -Dserver.port=9090; a value from application.yml is simply not in that table. Confusing the two produces the classic wrong conclusion “the value is configured but cannot be read” — use Environment or an annotation, not System.getProperty. For the same reason System.getenv("SERVER_PORT") sees only real OS environment variables, never yml.

Decision: three styles coexist in one team (@Value, a properties class, raw env.getProperty). Should they be unified?

- Unify on @Value — most obvious, smallest diff

- Business configuration goes into @ConfigurationProperties classes; only scattered framework values keep @Value

- Ban annotation-based config injection entirely and always call Environment.getProperty, so everyone "sees the truth"

Conclusion: B. A properties class can be validated with @Validated, completed by the IDE and unit-tested (just new it and set a few fields) — Environment gives you none of those three. A’s problem is that @Value does not participate in relaxed binding, so the moment one key must be overridden by an environment variable it fails silently (the comparison table in section 4 is that scene reproduced). C looks transparent but pushes conversion, defaults and validation back into hand-written code, which costs more, not less. Keep one exception: when the key name itself is only known at runtime (multi-tenant lookup by tenantId), Environment is the only option.

Analogy: the four ways are like four ways to ask for a price. @ConfigurationProperties photographs the whole menu into your phone (everything at once, and you can check for missing lines); @Value memorises one dish name and asks for it (one wrong character and the dish does not exist); Environment walks to the front desk and asks about anything on the spot (flexible, but you listen and convert the units yourself); System.getProperty rummages through an old receipt in your own pocket — it has no idea what the shop charges today.

Key point: one test decides it — is this configuration a group, and does it need constraints? If yes, properties class plus @Validated. A single scattered value is fine with @Value. Reach for Environment only when the key is chosen at runtime.

124 / 124
Summary

the configuration skeleton has three layers — carrier (properties / yml / yaml, all becoming PropertySources), precedence (command line > environment variables > external files > packaged files > defaults, the higher winning on the same key), and binding (@Value for single values, @ConfigurationProperties for groups with loose binding and validation). Multiple environments rely on Profiles: @Profile controls beans, application-{profile}.yml controls config, and there are four activation ways. Two iron rules to finish: always pair @ConfigurationProperties with @Validated and a validation implementation so bad config fails at startup, and inject secrets from environment variables to keep passwords out of the repository.