When a method is @Async, @Value variable becomes null in any other final methods (and, the @Async method itself if it's final) of the class.
Is this unavoidable due to Java Reflection spec?
Reproduction code here: https://github.com/taqqanori/spring-async-bug.
Reproduction code in short
@Service
public class AsyncService {
@Value("${spring.application.name}")
private String value;
@Async
public void async() {
// just declared, never called
}
public final void _final() {
// becomes null
System.out.println("async final: " + value);
}
public void nonFinal() {
// becomes "demo"
System.out.println("async non-final: " + value);
}
}
@Service
public class NonAsyncService {
@Value("${spring.application.name}")
private String value;
public final void _final() {
// becomes "demo"
System.out.println("non-async final: " + value);
}
public void nonFinal() {
// becomes "demo"
System.out.println("non-async non-final: " + value);
}
}
```:application.properties spring.application.name=demo
## Output
async final: null async non-final: demo non-async final: demo non-async non-final: demo ```
Environment
- OS: Ubuntu-20.04 on WSL2 on Windows11
- Java: openjdk-21
- Spring: Spring Boot 3.4.0 initialized from https://start.spring.io/ with "Spring Web" dependency
Thanks.
Comment From: taqqanori
I found @Retryable has the same problem https://github.com/spring-projects/spring-retry/issues/478
Comment From: ZLATAN628
Hi @taqqanori,
Here’s my understanding: When you use the @Async annotation, Spring creates a proxy object for the bean of the target class. Since the proxy is created using CGLIB, it generates a subclass of the target class and overrides the non-final methods to enable interception. As a result, when you invoke a final method, the call is handled directly by the proxy object itself, rather than being delegated to the original (target) object. Additionally, the proxy object does not copy the field values from the original target object.
Comment From: taqqanori
Hi @ZLATAN628
Thank you for the detailed explanation. Very interesting.
If this issue is unavoidable or too hard to fix, I'd like Spring framework to raise an Exception or warn in log, when @Async/@Retryable and @Value and final is used together in a class (or its ascendant and descendant).
I wasted hours wondering why my value is null...
Comment From: lucky8987
@taqqanori Generating proxy objects through cglib will skip the final modified class or method, but the log level is: trace, so you may not be able to see it. The corresponding code location is: org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#enhanceFactoryBean
Comment From: taqqanori
@lucky8987
Thank you for investigation.
I wanted to see that log at default log level, so that I could have noticed that final was doing something, before messing around with my code.
Comment From: jhoeller
We log at debug level for such scenarios in CglibAopProxy.doValidateClass. Maybe we should raise this to info level at least.
Comment From: jhoeller
As of 7.0, we log a public final method at warn level for the non-interface case as well now. If this turns out to cause too many spurious warnings for cases like final-marked setter methods on common classes in the Spring ecosystem, we need to either adapt those affected classes to drop final or fine-tune our warn condition.
Comment From: lcarilla
backporting this fix to framework 6 could IMO be considered. i stumbled across this, it cost me way too long figure out this was the issue, the warn log would have saved me quite some time :)