configuration-properties/ @ConfigurationProperties vs @Value on Spring Boot 4.1.1. The relaxed-binding matrix is generated by binding each spelling rather than transcribed, and re-checked against real processes -- the in-process probe was wrong twice before it was right. Records the three findings that came out of it: @Value does get relaxed resolution inside Spring Boot (Boot attaches ConfigurationPropertySources), the configuration processor silently stops generating metadata on JDK 23+ when declared as a plain dependency, and @Valid is not what makes nested constraints run. profiles-and-config/ Precedence, profiles, spring.config.import and config trees. /precedence reports every source holding a property in rank order with file and line, which turns "my profile file had no effect" into a two-line answer. Also pins the counterintuitive one: an imported file outranks the file that imported it. spring-aop/ Designators, proxy types, and aspects that do not fire. One advice per supported designator so the reference table is generated from real matches; all fourteen unsupported designators fed to the parser. Two corrections to the reference documentation: unsupported designators throw UnsupportedPointcutPrimitiveException (extends RuntimeException, not IllegalArgumentException), and spring-boot-starter-aop was renamed to spring-boot-starter-aspectj in Boot 4. 19 contract tests across the three modules, 15 captured transcripts, all regenerated by scripts/run-all.sh. Verified on Spring Boot 4.1.1, Spring Framework 7.0.9, JDK 25.0.4.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gip4srpzMwjgoba6uEfbr5
2.3 KiB
1. Two mechanisms, not two styles
@Value and @ConfigurationProperties are often presented as a matter of taste. They are not.
They are two different mechanisms that happen to read from the same Environment, and almost
every difference in behaviour follows from that.
@Value is placeholder resolution
@Value("${demo.mail.host}") is a string. Spring's PropertySourcesPlaceholderConfigurer
resolves it against the environment during bean post-processing, converts the result to the
field's type, and assigns it. There is no model of what a property is — only a name, a
lookup and a conversion.
Consequences:
- The property name lives in a string literal. Renaming a property is a text search.
- One field, one lookup. There is no way to express "these six properties belong together".
- Defaulting is
:inside the placeholder, and nothing else. - There is no validation step, because there is no object to validate.
- SpEL is available:
#{...}can compute rather than look up.
@ConfigurationProperties is binding
Binder walks a target type, works out which properties it needs, asks the
ConfigurationPropertySources for each one, converts, and constructs the object. The target
type is the model.
Consequences:
- The property names are derived from the type. Renaming a component renames the property.
- Nested types, lists and maps bind, because the binder knows the shape it is filling.
- Defaults are
@DefaultValue, applied per component during construction. - The bound object can be validated as an object, once, at startup.
- No SpEL. The binder maps values; it does not compute them.
The one that surprises people
Because both read from the same environment, the relaxed rules ought to differ — and in the Spring Framework alone they do. Inside Spring Boot they largely do not, because Boot attaches an extra property source that gives placeholder resolution the binder's name matching. Chapter 2 has the measured matrix.
Which to use
Use @ConfigurationProperties for anything that is a group of settings — which is most
things. Reach for @Value when you need a single value in a place where a type would be
ceremony, or when you need SpEL. Chapter 6 is the honest list.