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.
49 lines
2.1 KiB
Markdown
49 lines
2.1 KiB
Markdown
prev: [The self-invocation trap](02-the-self-invocation-trap.md) · [README](../README.md) · next: [Return types and exceptions](04-return-types-and-exceptions.md)
|
|
|
|
# 3. What the proxy cannot see
|
|
|
|
Spring Boot proxies with CGLIB by default, so the proxy is a generated *subclass* of your bean:
|
|
|
|
```
|
|
proxy class : com.ankurm.async.VisibilityService$$SpringCGLIB$$0
|
|
isCglibProxy: true
|
|
```
|
|
|
|
A subclass can override public and protected methods. It cannot override `final` ones, and it
|
|
cannot see `private` ones. `@Async` on either is inert, and Spring does not complain.
|
|
|
|
From [`docs/output/visibility.txt`](output/visibility.txt):
|
|
|
|
```
|
|
publicMethod() : task-3 (virtual=false)
|
|
finalMethod() : main (virtual=false)
|
|
protectedMethod() via the proxy : task-4 (virtual=false)
|
|
callProtectedInternally() : main (virtual=false)
|
|
```
|
|
|
|
Two things in that transcript are worth separating, because they look the same and are not:
|
|
|
|
- `finalMethod()` ran on `main` because **CGLIB could not override it**. Calling it from another
|
|
bean would not help.
|
|
- `callProtectedInternally()` ran on `main` because it is a **self-invocation** (chapter 2).
|
|
`protectedMethod()` itself is perfectly proxyable, and running on `task-4` when the test calls
|
|
it through the proxy proves it.
|
|
|
|
So "it ran on the caller's thread" has at least two distinct causes, and the fix differs. The
|
|
test in `VisibilityTest` can call a protected method at all only because it lives in the same
|
|
package as the service.
|
|
|
|
## The `final` class case
|
|
|
|
A `final` class cannot be subclassed either, so CGLIB cannot proxy it at all. That one *does*
|
|
fail loudly — context startup throws — which makes it much less dangerous than a `final` method.
|
|
|
|
## Kotlin
|
|
|
|
Kotlin classes and members are `final` unless declared `open`. A Kotlin service with `@Async` and
|
|
no `open` keyword, and no `kotlin-spring` compiler plugin, is the `final` case above. The
|
|
`kotlin-spring` plugin exists to open Spring-annotated classes automatically; it does not open
|
|
methods annotated only with `@Async` unless the class-level rule already applies.
|
|
|
|
next: [Return types and exceptions](04-return-types-and-exceptions.md)
|