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:
84
spring-aop/docs/05-broken-aspect-gallery.md
Normal file
84
spring-aop/docs/05-broken-aspect-gallery.md
Normal file
@@ -0,0 +1,84 @@
|
||||
[← Advice types](04-advice-types.md) · [Index](../README.md) · [Diagnosing →](06-diagnosing-a-silent-aspect.md)
|
||||
|
||||
# 5. Six aspects that do not fire
|
||||
|
||||
Sources: [`BrokenAspects`](../src/main/java/com/ankurm/aop/broken/BrokenAspects.java),
|
||||
[`SelfInvokingService`](../src/main/java/com/ankurm/aop/broken/SelfInvokingService.java),
|
||||
[`NewedUpService`](../src/main/java/com/ankurm/aop/broken/NewedUpService.java).
|
||||
Transcript: [`03-broken-gallery.txt`](output/03-broken-gallery.txt).
|
||||
|
||||
None of these warn. None fail at startup. All of them look correct in review.
|
||||
|
||||
## 1. `@Aspect` without `@Component`
|
||||
|
||||
`@Aspect` is an AspectJ annotation. It tells Spring how to interpret a bean it already has; it
|
||||
does not create one. Without a stereotype or an `@Bean` method, the class is never instantiated
|
||||
and the pointcut is never registered.
|
||||
|
||||
The most common cause, and the most invisible — from the container's point of view nothing was
|
||||
ever requested, so there is nothing to warn about.
|
||||
|
||||
**Fix:** add `@Component`.
|
||||
|
||||
## 2. A pointcut that matches nothing
|
||||
|
||||
```java
|
||||
@Before("execution(* com.ankurm.aop.services.*.*(..))") // "services", plural
|
||||
```
|
||||
|
||||
A package name matching no type is not an error; it is an empty match set. At runtime this is
|
||||
indistinguishable from an aspect that was never registered.
|
||||
|
||||
**Fix:** assert on it. `AspectJExpressionPointcut#matches(Method, Class)` in a unit test is two
|
||||
lines and catches every typo permanently.
|
||||
|
||||
## 3. A private method
|
||||
|
||||
Neither proxy strategy can override a private method, so neither can intercept it. The
|
||||
annotation is legal and inert.
|
||||
|
||||
**Fix:** make it at least package-private *and* call it from outside the object — visibility
|
||||
alone is not enough if the call is internal, which brings you to number 5.
|
||||
|
||||
## 4. A final method
|
||||
|
||||
CGLIB proxies by subclassing. A final method is inherited rather than overridden, so calls go
|
||||
straight to the original. A final *class* fails loudly; a final *method* is silent.
|
||||
|
||||
Note `beanIsProxied: true` in the transcript. The bean is proxied. This one method is not.
|
||||
|
||||
**Fix:** remove `final`, or proxy by interface.
|
||||
|
||||
## 5. Self-invocation
|
||||
|
||||
The expensive one, because the code looks right and the annotation is visible.
|
||||
|
||||
```json
|
||||
"5-self-invocation": {
|
||||
"beanIsProxied": true,
|
||||
"innerAdvisedWhenCalledFromOuter": false,
|
||||
"innerAdvisedWhenCalledDirectly": true
|
||||
}
|
||||
```
|
||||
|
||||
Same method, same advice. Called through the proxy it is advised; reached by `this.inner()`
|
||||
from another method of the same object it is not, because the proxy is not in that call path.
|
||||
|
||||
This is the same mechanism that makes `@Transactional` and `@Cacheable` silently do nothing on
|
||||
internal calls. Learning it once here saves learning it three times.
|
||||
|
||||
**Fixes, best first:**
|
||||
|
||||
1. Move the method to another bean. This is almost always the right answer, and the resulting
|
||||
design is usually better anyway.
|
||||
2. Inject the bean into itself and call through that reference.
|
||||
3. `AopContext.currentProxy()` with `exposeProxy = true`. Works; couples your code to Spring
|
||||
AOP and makes the class aware it is proxied.
|
||||
|
||||
## 6. An object created with `new`
|
||||
|
||||
Spring AOP advises beans. An instance built by a factory, a helper or a test has no proxy and
|
||||
never will.
|
||||
|
||||
**Fix:** get it from the container. If it genuinely must be constructed by hand and still
|
||||
advised, that is what AspectJ load-time weaving is for.
|
||||
Reference in New Issue
Block a user