1
0

Three new article modules: configuration binding, profiles and config data, Spring AOP

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
This commit is contained in:
2026-09-08 16:36:17 +00:00
parent 958b401f0f
commit 86246dc860
107 changed files with 5075 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
== every source sets demo.greeting at once ==
$ DEMO_GREETING=from-environment-variable \
java -Ddemo.greeting=from-system-property \
-jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod \
--demo.greeting=from-command-line-argument
{
"property": "demo.greeting",
"effectiveValue": "from-command-line-argument",
"activeProfiles": [
"prod",
"prod-db",
"prod-metrics"
],
"holders": [
{
"rank": 1,
"source": "SimpleCommandLinePropertySource {name='commandLineArgs'}",
"value": "from-command-line-argument",
"origin": "\"demo.greeting\" from property source \"commandLineArgs\""
},
{
"rank": 2,
"source": "PropertiesPropertySource {name='systemProperties'}",
"value": "from-system-property",
"origin": "\"demo.greeting\" from property source \"systemProperties\""
},
{
"rank": 3,
"source": "OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}",
"value": "from-environment-variable",
"origin": "System Environment Property \"DEMO_GREETING\""
},
{
"rank": 4,
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/''}",
"value": "from-application-prod-yaml",
"origin": "class path resource [application-prod.yaml] from profiles-and-config-1.0.0.jar - 4:13"
},
{
"rank": 5,
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/''}",
"value": "from-application-yaml",
"origin": "class path resource [application.yaml] from profiles-and-config-1.0.0.jar - 21:13"
}
],
"shadowedCount": 4
}
== and with the environment variable removed, nothing else changed ==
./scripts/demo-precedence.sh: line 41: 5735 Killed DEMO_GREETING=from-environment-variable setsid nohup java -Ddemo.greeting=from-system-property -jar "$JAR" --spring.profiles.active=prod --demo.greeting=from-command-line-argument > /tmp/profiles-precedence.log 2>&1 < /dev/null
{
"property": "demo.greeting",
"effectiveValue": "from-system-property",
"activeProfiles": [
"prod",
"prod-db",
"prod-metrics"
],
"holders": [
{
"rank": 1,
"source": "PropertiesPropertySource {name='systemProperties'}",
"value": "from-system-property",
"origin": "\"demo.greeting\" from property source \"systemProperties\""
},
{
"rank": 2,
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/''}",
"value": "from-application-prod-yaml",
"origin": "class path resource [application-prod.yaml] from profiles-and-config-1.0.0.jar - 4:13"
},
{
"rank": 3,
"source": "OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/''}",
"value": "from-application-yaml",
"origin": "class path resource [application.yaml] from profiles-and-config-1.0.0.jar - 21:13"
}
],
"shadowedCount": 2
}
./scripts/demo-precedence.sh: line 41: 5796 Killed setsid nohup java -Ddemo.greeting=from-system-property -jar "$JAR" --spring.profiles.active=prod > /tmp/profiles-precedence2.log 2>&1 < /dev/null

View File

@@ -0,0 +1,41 @@
== does application-prod.yaml win? ==
demo.datasource-url is set in application.yaml and again in application-prod.yaml.
--- 1. prod profile active, no environment variable ---
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod
active profiles : prod, prod-db, prod-metrics
effective value : jdbc:postgresql://prod-db:5432/orders
1. jdbc:postgresql://prod-db:5432/orders <- 'file application-prod.yaml' via location 'optional:classpath:/''}
2. jdbc:h2:mem:default <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 1
--- 2. identical, plus one leftover environment variable ---
$ DEMO_DATASOURCE_URL=jdbc:postgresql://leftover:5432/orders \
java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=prod
active profiles : prod, prod-db, prod-metrics
effective value : jdbc:postgresql://leftover:5432/orders
1. jdbc:postgresql://leftover:5432/orders <- OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
2. jdbc:postgresql://prod-db:5432/orders <- 'file application-prod.yaml' via location 'optional:classpath:/''}
3. jdbc:h2:mem:default <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 2
The profile-specific file is still loaded and still holds its value -- it is listed,
and it lost. Config data is item 3 in the documented precedence list; OS environment
variables are item 5, and later items win.
== the full property-source stack, in order ==
$ curl -s localhost:8080/sources
1. MapPropertySource server.ports
2. ConfigurationPropertySourcesPropertySource configurationProperties
3. SimpleCommandLinePropertySource commandLineArgs
4. StubPropertySource servletConfigInitParams
5. ServletContextPropertySource servletContextInitParams
6. PropertiesPropertySource systemProperties
7. OriginAwareSystemEnvironmentPropertySource systemEnvironment
8. RandomValuePropertySource random
9. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod-metrics.yaml]' via location 'optional:classpath:/'
10. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod-db.yaml]' via location 'optional:classpath:/'
11. OriginTrackedMapPropertySource Config resource 'class path resource [application-prod.yaml]' via location 'optional:classpath:/'
12. OriginTrackedMapPropertySource Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'
13. ApplicationInfoPropertySource applicationInfo

View File

@@ -0,0 +1,45 @@
== what Kubernetes actually mounts ==
$ find /tmp/demo-configmap /tmp/demo-secret -type f | sort
<configmap-mount>/demo.datasource-url
<configmap-mount>/demo.greeting
<configmap-mount>/demo.pool-size
<configmap-mount>/demo/nested/value
<secret-mount>/demo.api-key
$ cat <configmap-mount>/demo.greeting; echo
from-configmap-volume
Each file holds a bare value with no trailing newline and no key. There is no
properties syntax to parse -- the filename is the key.
== importing it ==
$ java -jar target/profiles-and-config-1.0.0.jar \
--spring.config.import=configtree:/tmp/demo-configmap/,configtree:/tmp/demo-secret/
active profiles : (none)
effective value : from-configmap-volume
1. from-configmap-volume <- ConfigTreePropertySource {name='Config tree '/tmp/demo-configmap''}
2. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 1
demo.pool-size = 25
demo.nested.value = from-nested-directory
demo.api-key = sk_live_not_a_real_key
A directory under the mount becomes a nested property: demo/nested/value is
demo.nested.value. That is how a ConfigMap with slashes in its keys arrives.
== the part that surprises people ==
An imported config tree outranks application.yaml, but it is still config data,
so it still loses to an environment variable:
active profiles : (none)
effective value : from-environment-variable
1. from-environment-variable <- OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
2. from-configmap-volume <- ConfigTreePropertySource {name='Config tree '/tmp/demo-configmap''}
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 2
There is also no such thing as a profile-specific config tree. There is no
<mount>-prod directory convention; a per-environment ConfigMap is a different mount
chosen by the deployment, not by spring.profiles.active.

View File

@@ -0,0 +1,60 @@
== spring.config.import: which document wins? ==
application-import.yaml imports imported.yaml. Both set demo.greeting.
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=import
active profiles : import
effective value : from-imported-yaml
1. from-imported-yaml <- 'file imported.yaml' via location 'optional:classpath:/imported.yaml''}
2. from-application-import-yaml <- 'file application-import.yaml' via location 'optional:classpath:/''}
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 2
demo.imported-only = yes-this-file-was-read
The imported file WON. spring.config.import does not behave like #include, and it
does not behave like a default either: the imported document is processed AFTER the
document that declared the import, so it outranks the file that pulled it in.
If you import a shared baseline expecting your own file to override it, every key
the baseline sets will quietly beat yours.
== one file, several documents, activated by condition ==
--- spring.profiles.active=<none> (with the multidoc profile) ---
active profiles : multidoc
effective value : from-multidoc-default-document
1. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
2. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 1
--- spring.profiles.active=staging (with the multidoc profile) ---
active profiles : multidoc, staging
effective value : from-multidoc-staging-document
1. from-multidoc-staging-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
2. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
3. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 2
--- spring.profiles.active=prod (with the multidoc profile) ---
active profiles : multidoc, prod, prod-db, prod-metrics
effective value : from-application-prod-yaml
1. from-application-prod-yaml <- 'file application-prod.yaml' via location 'optional:classpath:/''}
2. from-multidoc-prod-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
3. from-multidoc-default-document <- 'file application-multidoc.yaml' via location 'optional:classpath:/' (document ...
4. from-application-yaml <- 'file application.yaml' via location 'optional:classpath:/''}
holders that lost: 3
Later documents in the same file win over earlier ones, so the unconditional first
document acts as the default and each conditional document overrides it.
== the activation Spring Boot refuses ==
application-badactivation.yaml tries to set spring.profiles.active from a document
that is itself conditional on a profile.
$ java -jar target/profiles-and-config-1.0.0.jar --spring.profiles.active=badactivation,staging
org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property
'spring.profiles.active' imported from location 'class path resource
[application-badactivation.yaml]' is invalid in a profile specific resource [origin: class path
resource [application-badactivation.yaml] from profiles-and-config-1.0.0.jar - 12:13]
at
org.springframework.boot.context.config.InvalidConfigDataPropertyException.lambda$throwIfPropert
yFound$1(InvalidConfigDataPropertyException.java:123)