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.
2.1 KiB
prev: The self-invocation trap · README · next: Return types and exceptions
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:
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 onmainbecause CGLIB could not override it. Calling it from another bean would not help.callProtectedInternally()ran onmainbecause it is a self-invocation (chapter 2).protectedMethod()itself is perfectly proxyable, and running ontask-4when 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.