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,11 @@
== versions ==
openjdk version "25.0.4.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode, sharing)
spring-boot-starter-parent: 4.1.1
aspectjweaver: 1.9.25.1
== the Boot 4 starter rename ==
spring-boot-starter-aop last published: 4.0.0-M2 (last GA 3.5.16)
spring-boot-starter-aspectj first published: 4.0.0-M3

View File

@@ -0,0 +1,59 @@
== which designator matched which join point ==
Five methods are called once each: OrderService.place, OrderService.cancel,
InventoryService.reserve, InventoryService.finalCheck and
DefaultOrderService.interfaceless.
orderService proxy kind : CGLIB subclass
proxy is an instance of DefaultOrderService : True
args (bound: SKU-1/2) -> DefaultOrderService.place(..)
bean(*OrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
@within(Audited) -> DefaultOrderService.place(..)
execution (full signature) -> DefaultOrderService.place(..)
@annotation(Marker) -> DefaultOrderService.place(..)
target(DefaultOrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
this(OrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
execution (wildcards) -> DefaultOrderService.cancel(..)
@target(Audited) -> DefaultOrderService.cancel(..)
bean(inventoryService) -> InventoryService.reserve(..)
within -> InventoryService.reserve(..)
@args(Trackable) -> DefaultOrderService.interfaceless(..)
== what the parser accepts and refuses ==
supported (all parsed and evaluated):
execution(* com.ankurm.aop.service.OrderService.place(..)) OK
within(com.ankurm.aop.service..*) OK
this(com.ankurm.aop.service.OrderService) OK
target(com.ankurm.aop.service.DefaultOrderService) OK
args(String, int) OK
@target(com.ankurm.aop.service.Audited) OK
@args(com.ankurm.aop.service.Trackable) OK
@within(com.ankurm.aop.service.Audited) OK
@annotation(com.ankurm.aop.service.Marker) OK
bean(defaultOrderService) OK
unsupported in Spring AOP:
call(* com.ankurm.aop.service.OrderService.place(. rejected
get(* com.ankurm.aop.service.*.*) rejected
set(* com.ankurm.aop.service.*.*) rejected
initialization(com.ankurm.aop.service.*.new(..)) rejected
staticinitialization(com.ankurm.aop.service.*) rejected
preinitialization(com.ankurm.aop.service.*.new(..) rejected
handler(java.lang.Exception) rejected
adviceexecution() rejected
withincode(* com.ankurm.aop.service.*.*(..)) rejected
cflow(execution(* com.ankurm.aop.service.*.*(..))) rejected
cflowbelow(execution(* com.ankurm.aop.service.*.*( rejected
if() rejected
@this(com.ankurm.aop.service.Audited) rejected
@withincode(com.ankurm.aop.service.Marker) rejected
the exception, in full:
org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException
Pointcut expression 'call(* com.ankurm.aop.service.OrderService.place(..))' contains unsupported pointcut primitive 'call'
The reference documentation says these produce an IllegalArgumentException. They do
not: UnsupportedPointcutPrimitiveException extends RuntimeException directly, so a
catch of IllegalArgumentException will not catch it.

View File

@@ -0,0 +1,53 @@
== Spring Boot default: spring.aop.proxy-target-class=true ==
$ java -jar target/spring-aop-demo-1.0.0.jar
defaultOrderService CGLIB subclass target=DefaultOrderService
class : DefaultOrderService$$SpringCGLIB$$0
interfaces : (none)
advisors : 11
inventoryService CGLIB subclass target=InventoryService
class : InventoryService$$SpringCGLIB$$0
interfaces : (none)
advisors : 6
selfInvokingService CGLIB subclass target=SelfInvokingService
class : SelfInvokingService$$SpringCGLIB$$0
interfaces : (none)
advisors : 2
proxy is an instance of DefaultOrderService : True
this(OrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
target(DefaultOrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
bean(*OrderService) -> DefaultOrderService.place(..), DefaultOrderService.cancel(..), DefaultOrderService.interfaceless(..)
== framework default restored: spring.aop.proxy-target-class=false ==
$ java -jar target/spring-aop-demo-1.0.0.jar --spring.aop.proxy-target-class=false
defaultOrderService JDK dynamic proxy target=DefaultOrderService
class : $Proxy62
interfaces : OrderService
advisors : 11
inventoryService CGLIB subclass target=InventoryService
class : InventoryService$$SpringCGLIB$$0
interfaces : (none)
advisors : 6
selfInvokingService CGLIB subclass target=SelfInvokingService
class : SelfInvokingService$$SpringCGLIB$$0
interfaces : (none)
advisors : 2
proxy is an instance of DefaultOrderService : False
this(OrderService) -> OrderService.place(..), OrderService.cancel(..)
target(DefaultOrderService) -> OrderService.place(..), OrderService.cancel(..)
bean(*OrderService) -> OrderService.place(..), OrderService.cancel(..)
Same aspects, same beans, different proxy strategy:
- With CGLIB the proxy is a SUBCLASS of DefaultOrderService, so it is an instance of
the implementation class and methods that are not on the interface are advised.
- With a JDK proxy the proxy implements OrderService only. It is NOT an instance of
DefaultOrderService, casting to that class throws ClassCastException, and any
method absent from the interface is invisible to advice.
This is why this() and target() differ. this() tests the proxy; target() tests the
object behind it. Under CGLIB they usually agree, which is exactly why the
distinction only bites after somebody switches the proxy type.

View File

@@ -0,0 +1,37 @@
== aspects that do not fire ==
{
"5-self-invocation": {
"beanIsProxied": true,
"result": "outer -> inner",
"innerAdvisedWhenCalledFromOuter": false,
"innerAdvisedWhenCalledDirectly": true
},
"6-created-with-new": {
"isProxy": false,
"adviceFired": false
},
"4-final-method": {
"beanIsProxied": true,
"proxyKind": "CGLIB subclass",
"adviceFired": false
},
"1-aspect-without-component-fired": false,
"2-pointcut-typo-fired": false,
"3-private-method-fired": false,
"note": "every value above should be false except the two that prove the method is advisable when reached through the proxy"
}
Reading it:
1 @Aspect without @Component - the class is never instantiated, so the pointcut
is never registered. No warning is produced.
2 pointcut typo - 'com.ankurm.aop.services' (plural) parses fine and
matches nothing. An empty match set is not an error.
3 private method - cannot be overridden, so cannot be intercepted.
4 final method - CGLIB subclasses; a final method is inherited, not
overridden. Note beanIsProxied is still true.
5 self-invocation - innerAdvisedWhenCalledFromOuter is false and
innerAdvisedWhenCalledDirectly is true. Same method,
same advice: only the call path differs.
6 created with new - no container, no proxy, no advice.