Files
spring-async-demo/async/docs/02-the-self-invocation-trap.md
Ankur Mhatre 9b3fe88813 Add the async module
Every @Async behaviour that surprises people, asserted by a test and captured
to docs/output/: the self-invocation trap, what CGLIB cannot override, the
IllegalArgumentException a plain return type throws, the unbounded queue that
makes max-size decoration, spring.task.execution.propagate-context (new in
Boot 4.1.0), the two Executor beans that leave @Async on an unpooled
SimpleAsyncTaskExecutor, and JEP 491 measured on JDK 21 against JDK 25.
2026-09-01 23:32:16 +05:30

64 lines
2.6 KiB
Markdown

prev: [What `@Async` actually does](01-what-async-actually-does.md) · [README](../README.md) · next: [What the proxy cannot see](03-what-the-proxy-cannot-see.md)
# 2. The self-invocation trap
The proxy wraps the bean. It does not replace `this`.
```java
@Async
public CompletableFuture<String> annotated() { ... }
public CompletableFuture<String> viaSelfInvocation() {
return annotated(); // this.annotated() — the proxy is not involved
}
```
When another bean calls `service.annotated()`, it holds a reference to the *proxy*, so the call
is intercepted. When `viaSelfInvocation()` calls `annotated()`, the compiler emits
`this.annotated()`, and `this` inside the method body is the *target* object, not the proxy.
There is no interception, so the method runs inline and the `CompletableFuture` you get back is
already complete.
Measured, in [`docs/output/self-invocation.txt`](output/self-invocation.txt):
```
service.annotated() : task-5 (virtual=false)
service.viaSelfInvocation() : main (virtual=false)
service.viaSelfReference() : task-6 (virtual=false)
```
Nothing is logged. The method returns a valid future with the correct value in it. The only
symptom is that the caller waited.
## The fixes, in the order you should prefer them
**Move the method to another bean.** If work is asynchronous, it belongs behind a boundary, and
the boundary is a good place for a class. This is the fix that survives a refactor.
**Go back out through the proxy on purpose.** `SelfInvocationService` does this with an
`ObjectProvider<SelfInvocationService>`:
```java
public CompletableFuture<String> viaSelfReference() {
return self.getObject().annotated();
}
```
`ObjectProvider` is lazy, so there is no circular dependency to resolve at construction time.
Injecting `SelfInvocationService` directly also works — Spring special-cases self-references —
but it looks like a bug to every reviewer who has not read this chapter.
**`@EnableAsync(mode = AdviceMode.ASPECTJ)`** removes the limitation entirely, because
load-time weaving rewrites the method itself rather than wrapping the object. It also adds an
agent to your startup and a weaving configuration to your build. Very few applications should
pay that.
## Why this keeps happening
The trap is not that people do not know about proxies. It is that the calling code often did not
start out in the same class. A method gets extracted, a controller's logic moves into the
service that already had the `@Async` method, and one day a call that used to arrive from outside
arrives from inside. Nothing fails. The throughput just changes.
next: [What the proxy cannot see](03-what-the-proxy-cannot-see.md)