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.
This commit is contained in:
63
async/docs/02-the-self-invocation-trap.md
Normal file
63
async/docs/02-the-self-invocation-trap.md
Normal file
@@ -0,0 +1,63 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user