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.
51 lines
2.6 KiB
Markdown
51 lines
2.6 KiB
Markdown
[README](../README.md) · next: [The self-invocation trap](02-the-self-invocation-trap.md)
|
|
|
|
# 1. What `@Async` actually does
|
|
|
|
`@Async` is not a keyword and it is not a thread. It is a marker that
|
|
[`AsyncAnnotationBeanPostProcessor`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.html)
|
|
looks for while the context is being built. When it finds one, it wraps the bean in a proxy. The
|
|
proxy's version of the method does not call your code; it wraps your code in a `Callable`, hands
|
|
that to an `AsyncTaskExecutor`, and returns immediately.
|
|
|
|
Three consequences follow from that sentence, and between them they explain almost every
|
|
`@Async` question ever asked:
|
|
|
|
1. **The proxy is the whole mechanism.** A call that does not go through the proxy is not
|
|
asynchronous. See [chapter 2](02-the-self-invocation-trap.md) and
|
|
[chapter 3](03-what-the-proxy-cannot-see.md).
|
|
2. **The return value has to be produced before your code runs.** So the return type is
|
|
constrained, and exceptions have nowhere obvious to go. See
|
|
[chapter 4](04-return-types-and-exceptions.md).
|
|
3. **Which executor gets the `Callable` is resolved separately, by type and then by name.** It is
|
|
not necessarily the one you configured. See [chapter 7](07-which-executor-runs-it.md).
|
|
|
|
## `@EnableAsync` is not automatic
|
|
|
|
Spring Boot auto-configures the *executor*. It does not enable the *annotation*. Without
|
|
`@EnableAsync` somewhere in the context, no post-processor is installed, no proxy is created, and
|
|
every `@Async` method in the application runs on its caller's thread. There is no warning at any
|
|
log level, because from Spring's point of view nothing unusual has happened — you have a bean
|
|
with an annotation nobody asked it to process.
|
|
|
|
`AsyncDemoApplication` carries the annotation for exactly this reason.
|
|
|
|
## The evidence in this module
|
|
|
|
Every claim in these chapters is asserted by a test and captured in `docs/output/`. The
|
|
measurement is always the same one: the name of the thread the method body actually ran on,
|
|
returned from the method itself.
|
|
|
|
```
|
|
caller thread : main (virtual=false)
|
|
service.annotated() : task-5 (virtual=false)
|
|
service.viaSelfInvocation() : main (virtual=false)
|
|
service.viaSelfReference() : task-6 (virtual=false)
|
|
```
|
|
|
|
Timing cannot tell you this. A method that runs synchronously in 3 ms and a method that runs on a
|
|
pool thread in 3 ms look identical from the outside, which is why `@Async` failures survive so
|
|
long in production.
|
|
|
|
next: [The self-invocation trap](02-the-self-invocation-trap.md)
|