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:
51
spring-aop/docs/01-what-spring-aop-is.md
Normal file
51
spring-aop/docs/01-what-spring-aop-is.md
Normal file
@@ -0,0 +1,51 @@
|
||||
[Index](../README.md) · [Designators →](02-designators.md)
|
||||
|
||||
# 1. What Spring AOP actually is
|
||||
|
||||
Spring AOP is a **proxy** mechanism that borrows AspectJ's **pointcut language**. Both halves
|
||||
of that sentence explain a failure mode.
|
||||
|
||||
## It is proxies
|
||||
|
||||
Spring does not modify your bytecode. When a bean matches a pointcut, the container puts a
|
||||
proxy in the bean registry in its place. Callers get the proxy; the proxy runs advice and then
|
||||
forwards to the real object.
|
||||
|
||||
Everything that follows from this:
|
||||
|
||||
- Only **beans** can be advised. An object created with `new` has no proxy.
|
||||
- Only calls **through the proxy** are intercepted. A call the object makes to itself is not.
|
||||
- Only **overridable** methods can be advised: not private, not final, not static.
|
||||
- Join points are **method executions**. Field access, constructor calls and exception handlers
|
||||
are not available, no matter what the pointcut language allows you to write.
|
||||
|
||||
## It borrows the pointcut language
|
||||
|
||||
`aspectjweaver` on the classpath supplies the pointcut *parser*. Spring uses it to decide which
|
||||
methods match and then does its own weaving with proxies. AspectJ's own weaver is not involved.
|
||||
|
||||
This is why the designator list is a subset: the language can express `call()` and `cflow()`,
|
||||
and a proxy cannot implement them. See [chapter 2](02-designators.md).
|
||||
|
||||
## The Spring Boot 4 starter rename
|
||||
|
||||
In Spring Boot 3 the dependency was `spring-boot-starter-aop`. In Spring Boot 4 it is
|
||||
**`spring-boot-starter-aspectj`**.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
The contents are unchanged: `spring-boot-starter`, `spring-aop`, `aspectjweaver`. But the old
|
||||
artifact is no longer in the Boot BOM, so copying a dependency block out of any pre-4 tutorial
|
||||
fails resolution with a missing-version error rather than a helpful message.
|
||||
|
||||
Without the starter, `@Aspect` classes are ordinary beans, no pointcut is ever parsed, and
|
||||
every aspect in the application silently matches nothing.
|
||||
|
||||
## When to use something else
|
||||
|
||||
If you need to advise field access, constructors, or calls between objects you do not own, you
|
||||
need real AspectJ weaving (compile-time or load-time), not Spring AOP. If you need to advise
|
||||
one internal call, you need to refactor — see [chapter 5](05-broken-aspect-gallery.md).
|
||||
86
spring-aop/docs/02-designators.md
Normal file
86
spring-aop/docs/02-designators.md
Normal file
@@ -0,0 +1,86 @@
|
||||
[← What Spring AOP is](01-what-spring-aop-is.md) · [Index](../README.md) · [Proxy types →](03-proxy-types.md)
|
||||
|
||||
# 2. The designator reference
|
||||
|
||||
Generated by [`DesignatorAspect`](../src/main/java/com/ankurm/aop/aspect/DesignatorAspect.java)
|
||||
and [`PointcutParserEndpoint`](../src/main/java/com/ankurm/aop/web/PointcutParserEndpoint.java).
|
||||
Transcript: [`01-designators.txt`](output/01-designators.txt).
|
||||
|
||||
## Supported
|
||||
|
||||
| Designator | Matches on | Evaluated | Cost |
|
||||
|---|---|---|---|
|
||||
| `execution(...)` | method signature | statically | cheap |
|
||||
| `within(Type)` | the declaring type | statically | cheap |
|
||||
| `this(Type)` | the **proxy**'s type | at runtime | per call |
|
||||
| `target(Type)` | the **target object**'s type | at runtime | per call |
|
||||
| `args(Types)` | argument runtime types; can bind | at runtime | per call |
|
||||
| `@target(Ann)` | annotation on the executing object's class | at runtime | per call |
|
||||
| `@args(Ann)` | annotation on argument runtime types | at runtime | per call |
|
||||
| `@within(Ann)` | annotation on the declaring type | statically | cheap |
|
||||
| `@annotation(Ann)` | annotation on the method | statically | cheap |
|
||||
| `bean(name)` | Spring bean name, wildcards allowed | — | cheap |
|
||||
|
||||
`bean(...)` is Spring's own; it does not exist in AspectJ.
|
||||
|
||||
The runtime group cannot be decided from the signature alone, so Spring must check on every
|
||||
candidate invocation. Prefer the static equivalent where one exists: `@within` instead of
|
||||
`@target`, `within` instead of `this`, when the distinction does not matter.
|
||||
|
||||
## What each one actually matched here
|
||||
|
||||
```
|
||||
execution (full signature) -> DefaultOrderService.place(..)
|
||||
execution (wildcards) -> DefaultOrderService.cancel(..)
|
||||
within -> InventoryService.reserve(..)
|
||||
this(OrderService) -> place, cancel, interfaceless
|
||||
target(DefaultOrderService) -> place, cancel, interfaceless
|
||||
args (bound: SKU-1/2) -> DefaultOrderService.place(..)
|
||||
@target(Audited) -> DefaultOrderService.cancel(..)
|
||||
@args(Trackable) -> DefaultOrderService.interfaceless(..)
|
||||
@within(Audited) -> DefaultOrderService.place(..)
|
||||
@annotation(Marker) -> DefaultOrderService.place(..)
|
||||
bean(inventoryService) -> InventoryService.reserve(..)
|
||||
bean(*OrderService) -> place, cancel, interfaceless
|
||||
```
|
||||
|
||||
`InventoryService.finalCheck` was called and appears nowhere: it is `final`, so no proxy could
|
||||
override it. That is [failure 4](05-broken-aspect-gallery.md).
|
||||
|
||||
## Not supported
|
||||
|
||||
`call`, `get`, `set`, `preinitialization`, `staticinitialization`, `initialization`, `handler`,
|
||||
`adviceexecution`, `withincode`, `cflow`, `cflowbelow`, `if`, `@this`, `@withincode`.
|
||||
|
||||
All fourteen were fed to the parser. All fourteen were rejected, with:
|
||||
|
||||
```
|
||||
org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException
|
||||
Pointcut expression 'call(* ...place(..))' contains unsupported pointcut primitive 'call'
|
||||
```
|
||||
|
||||
**The reference documentation says these throw `IllegalArgumentException`. They do not.**
|
||||
`UnsupportedPointcutPrimitiveException extends RuntimeException` directly — checked with
|
||||
`javap`, pinned by `AopContractTests.unsupportedDesignatorExceptionType`. A
|
||||
`catch (IllegalArgumentException)` will not catch it.
|
||||
|
||||
## When the failure happens
|
||||
|
||||
`setExpression(...)` only stores the string. The expression is not parsed or validated until
|
||||
something asks it to match. An unsupported designator therefore fails at the first candidate
|
||||
invocation, not at startup — so a rarely-exercised aspect can ship broken.
|
||||
|
||||
## Combining and naming
|
||||
|
||||
`&&`, `||` and `!` compose designators. Name the result rather than repeating it:
|
||||
|
||||
```java
|
||||
@Pointcut("within(com.ankurm.aop.service..*)")
|
||||
public void inServiceLayer() {}
|
||||
|
||||
@Before("this(OrderService) && inServiceLayer()")
|
||||
public void advice(JoinPoint jp) { }
|
||||
```
|
||||
|
||||
A named pointcut is referenced by its method name and is the single biggest readability win
|
||||
available here.
|
||||
78
spring-aop/docs/03-proxy-types.md
Normal file
78
spring-aop/docs/03-proxy-types.md
Normal file
@@ -0,0 +1,78 @@
|
||||
[← Designators](02-designators.md) · [Index](../README.md) · [Advice types →](04-advice-types.md)
|
||||
|
||||
# 3. JDK dynamic proxies and CGLIB
|
||||
|
||||
Transcript: [`02-proxy-types.txt`](output/02-proxy-types.txt).
|
||||
|
||||
| | JDK dynamic proxy | CGLIB |
|
||||
|---|---|---|
|
||||
| Built by | `java.lang.reflect.Proxy` | subclassing the target |
|
||||
| Requires | at least one interface | a non-final class |
|
||||
| Proxy is an instance of | the interfaces only | the target class |
|
||||
| Can advise | public interface methods | public, protected, package-private |
|
||||
| Cannot advise | anything not on the interface | `final`, `private`, `static` |
|
||||
| Class name | `$Proxy62` | `Foo$$SpringCGLIB$$0` |
|
||||
|
||||
## Spring Boot chooses CGLIB
|
||||
|
||||
The Spring Framework's own default is interface-based when an interface exists. **Spring Boot
|
||||
sets `spring.aop.proxy-target-class=true`**, so you get CGLIB either way unless you change it.
|
||||
|
||||
Measured, on a bean that *does* implement an interface:
|
||||
|
||||
```
|
||||
defaultOrderService CGLIB subclass class: DefaultOrderService$$SpringCGLIB$$0
|
||||
interfaces : (none)
|
||||
proxy is an instance of DefaultOrderService : True
|
||||
```
|
||||
|
||||
And with `--spring.aop.proxy-target-class=false`:
|
||||
|
||||
```
|
||||
defaultOrderService JDK dynamic proxy class: $Proxy62
|
||||
interfaces : OrderService
|
||||
proxy is an instance of DefaultOrderService : False
|
||||
```
|
||||
|
||||
## What changes when you switch
|
||||
|
||||
Same aspects, same beans:
|
||||
|
||||
```
|
||||
CGLIB: this(OrderService) -> place, cancel, interfaceless
|
||||
JDK: this(OrderService) -> place, cancel
|
||||
```
|
||||
|
||||
`interfaceless(..)` is not on `OrderService`, so under a JDK proxy it is invisible to advice —
|
||||
and casting the bean to `DefaultOrderService` throws `ClassCastException`.
|
||||
|
||||
This is why `this()` and `target()` exist as separate designators. `this()` tests the proxy;
|
||||
`target()` tests the object behind it. Under CGLIB they nearly always agree, which is exactly
|
||||
why the distinction only ever bites *after* somebody changes the proxy type.
|
||||
|
||||
## CGLIB constraints worth knowing
|
||||
|
||||
- A **final class** cannot be proxied at all — this one fails loudly.
|
||||
- A **final method** is silently not advised. The bean is still a proxy; the method just is not
|
||||
overridden. This is the quiet one.
|
||||
- **Private** methods are never advised by either strategy.
|
||||
- The target's constructor is **not** called twice: Spring creates the proxy instance through
|
||||
Objenesis. On a JVM that forbids constructor bypassing you may see double invocation and a
|
||||
debug log line about it.
|
||||
- On the module path, classes in `java.lang` cannot be proxied without
|
||||
`--add-opens=java.base/java.lang=ALL-UNNAMED`.
|
||||
|
||||
## `@Proxyable`, new in Spring 7.0
|
||||
|
||||
Per-bean control, rather than one global switch:
|
||||
|
||||
```java
|
||||
@Proxyable(ProxyType.INTERFACES)
|
||||
@Bean
|
||||
MyService myService() { ... }
|
||||
```
|
||||
|
||||
Verified against the class file rather than the docs:
|
||||
`org.springframework.context.annotation.Proxyable`, targets `TYPE` and `METHOD`, with
|
||||
`ProxyType value()` (`DEFAULT`, `INTERFACES`, `TARGET_CLASS`) and `Class<?>[] interfaces()`.
|
||||
Note the package — it is in `spring-context`, not `spring-aop`.
|
||||
56
spring-aop/docs/04-advice-types.md
Normal file
56
spring-aop/docs/04-advice-types.md
Normal file
@@ -0,0 +1,56 @@
|
||||
[← Proxy types](03-proxy-types.md) · [Index](../README.md) · [Broken aspect gallery →](05-broken-aspect-gallery.md)
|
||||
|
||||
# 4. Advice types and ordering
|
||||
|
||||
## The five
|
||||
|
||||
| Annotation | Runs | Can it stop the call? | Can it change the result? |
|
||||
|---|---|---|---|
|
||||
| `@Before` | before | only by throwing | no |
|
||||
| `@AfterReturning` | after a normal return | no | no (can read it) |
|
||||
| `@AfterThrowing` | after an exception | no | no (can read it) |
|
||||
| `@After` | after either | no | no |
|
||||
| `@Around` | wraps | yes | yes |
|
||||
|
||||
`@Around` is the only one that receives a `ProceedingJoinPoint` and therefore the only one that
|
||||
can decide whether the target runs at all, retry it, cache around it, or replace its result.
|
||||
|
||||
```java
|
||||
@Around("@annotation(Timed)")
|
||||
public Object time(ProceedingJoinPoint pjp) throws Throwable {
|
||||
long start = System.nanoTime();
|
||||
try {
|
||||
return pjp.proceed();
|
||||
} finally {
|
||||
record(pjp.getSignature(), System.nanoTime() - start);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two rules for `@Around`: it must return `Object` (or a compatible type) and it must actually
|
||||
call `proceed()`. Forgetting `proceed()` silently turns every advised method into a method that
|
||||
returns `null` and never runs — a failure that looks like the target method being broken.
|
||||
|
||||
## Ordering
|
||||
|
||||
Within one aspect, advice order for the same join point is **not** guaranteed and is not the
|
||||
source order. If two pieces of advice in one aspect must be ordered, split them into two
|
||||
aspects.
|
||||
|
||||
Between aspects, `@Order` or `Ordered` decides. Lower value = higher precedence = outermost.
|
||||
Advice nests: the outermost aspect's `@Before` runs first and its `@After` runs last.
|
||||
|
||||
```
|
||||
@Order(1) Tx : before ... [ @Order(2) Logging : before ... target ... after ] ... after
|
||||
```
|
||||
|
||||
This matters when combining transactions with anything that must be inside or outside them.
|
||||
`@Transactional` is ordered at `Ordered.LOWEST_PRECEDENCE` by default, so almost everything
|
||||
else wraps *outside* it — meaning your logging advice sees the method return before the
|
||||
transaction commits, and therefore before a commit failure has happened.
|
||||
|
||||
## `ExposeInvocationInterceptor`
|
||||
|
||||
You will see this at the head of every advisor chain in
|
||||
[`02-proxy-types.txt`](output/02-proxy-types.txt). Spring adds it automatically so that
|
||||
`AopContext.currentProxy()` and argument binding work. It is not something you configured.
|
||||
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.
|
||||
75
spring-aop/docs/06-diagnosing-a-silent-aspect.md
Normal file
75
spring-aop/docs/06-diagnosing-a-silent-aspect.md
Normal file
@@ -0,0 +1,75 @@
|
||||
[← Broken aspect gallery](05-broken-aspect-gallery.md) · [Index](../README.md)
|
||||
|
||||
# 6. Diagnosing a silent aspect
|
||||
|
||||
Endpoint: [`AopDiagnosticsEndpoint`](../src/main/java/com/ankurm/aop/web/AopDiagnosticsEndpoint.java).
|
||||
|
||||
Almost every non-firing aspect is one of three things, and all three are visible in one place.
|
||||
|
||||
## The three questions, in order
|
||||
|
||||
**1. Is the bean proxied at all?**
|
||||
|
||||
```java
|
||||
AopUtils.isAopProxy(bean)
|
||||
```
|
||||
|
||||
`false` means no pointcut matched this bean, or it is not a bean. Stop here and check the
|
||||
pointcut and the registration.
|
||||
|
||||
**2. Which kind of proxy?**
|
||||
|
||||
```java
|
||||
AopUtils.isJdkDynamicProxy(bean) // interfaces only
|
||||
AopUtils.isCglibProxy(bean) // subclass
|
||||
```
|
||||
|
||||
If it is a JDK proxy and your method is not on an interface, that is your answer.
|
||||
|
||||
**3. Is your advice in the advisor list?**
|
||||
|
||||
```java
|
||||
if (bean instanceof Advised advised) {
|
||||
Arrays.stream(advised.getAdvisors())
|
||||
.map(a -> a.getAdvice().getClass().getSimpleName())
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
```
|
||||
|
||||
Proxied, right kind, and your advice missing means the pointcut matched the *bean* but not the
|
||||
*method*.
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
defaultOrderService CGLIB subclass target=DefaultOrderService
|
||||
class : DefaultOrderService$$SpringCGLIB$$0
|
||||
interfaces : (none)
|
||||
advisors : 11
|
||||
```
|
||||
|
||||
## Testing a pointcut without an application
|
||||
|
||||
The fastest check of all, and it belongs in a test rather than in a diagnostic endpoint:
|
||||
|
||||
```java
|
||||
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
|
||||
pointcut.setExpression("execution(* com.example.service.*.*(..))");
|
||||
assertThat(pointcut.matches(Foo.class.getMethod("bar"), Foo.class)).isTrue();
|
||||
```
|
||||
|
||||
Remember that `setExpression` does not parse; the parse happens on first `matches`. So this is
|
||||
also how you find out that a designator is unsupported before production does.
|
||||
|
||||
## Logging
|
||||
|
||||
```
|
||||
logging.level.org.springframework.aop=DEBUG
|
||||
```
|
||||
|
||||
reports proxy creation per bean. Verbose, but it answers question 1 for every bean at once.
|
||||
|
||||
## Delete the endpoint before shipping
|
||||
|
||||
It reports internal wiring. If you want it permanently, put it behind the management port and
|
||||
authentication.
|
||||
11
spring-aop/docs/output/00-versions.txt
Normal file
11
spring-aop/docs/output/00-versions.txt
Normal 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
|
||||
59
spring-aop/docs/output/01-designators.txt
Normal file
59
spring-aop/docs/output/01-designators.txt
Normal 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.
|
||||
53
spring-aop/docs/output/02-proxy-types.txt
Normal file
53
spring-aop/docs/output/02-proxy-types.txt
Normal 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.
|
||||
37
spring-aop/docs/output/03-broken-gallery.txt
Normal file
37
spring-aop/docs/output/03-broken-gallery.txt
Normal 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.
|
||||
Reference in New Issue
Block a user