Files
spring-async-demo/async/docs/03-what-the-proxy-cannot-see.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

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)