Kotlin中Spring Boot@Cacheable导致父类属性为空的原因探究
Kotlin中@Cacheable触发CGLIB代理导致父类属性为null的原因
问题复现
在Kotlin代码中,将Test2通过构造函数传入父类Test3,当注入Test1并调用test()方法时,父类中的test2属性输出为null:
@Service class Test1(test2: Test2) : Test3(test2) { @Cacheable(value = ["cacheKey"], key = "#key") fun cache(key: String): String { return "" } } @Service class Test2 { } open class Test3(val test2: Test2) { fun test() { println(test2) } }
但相同逻辑的Java代码无此问题:
@Service public class JTest1 extends JTest3 { public JTest1(JTest2 JTest2) { super(JTest2); } @Cacheable(value = "cacheKey", key = "#key") public String cache(String key) { return ""; } } @Service public class JTest2 { } public class JTest3 { private final JTest2 JTest2; public JTest3(JTest2 JTest2) { this.JTest2 = JTest2; } public void test() { System.out.println(JTest2); } }
已确认问题由@Cacheable触发的Spring CGLIB代理导致,核心差异在于Kotlin与Java的类结构和Spring代理的处理逻辑:
根本原因
Kotlin默认的final修饰符特性
Kotlin中,类、属性的getter方法、普通方法默认都是final的(除非显式添加open关键字)。即使通过kotlin-spring插件自动将@Service注解的类和被代理方法(如cache)改为open,父类Test3的test2属性getter、test()方法仍为final。而Java中,类、方法默认是可继承的(非final),父类的字段访问逻辑不受final限制。CGLIB代理的局限性与Spring的处理逻辑
CGLIB通过生成目标类的子类实现代理,无法覆盖final方法。当调用代理对象的test()方法时,由于该方法是父类的final方法,代理类无法拦截,直接执行父类的test()逻辑。
部分场景下Spring会使用Objenesis跳过构造函数创建Kotlin类的代理实例,导致代理对象的父类test2字段未被初始化,最终输出null。而Java场景中,父类方法和属性访问逻辑不受final限制,CGLIB代理可正常处理构造函数初始化和字段访问,因此不会出现null问题。
解决方案
- 显式将父类
Test3的test()方法改为open,同时将test2属性声明为open val test2: Test2,让CGLIB能正确代理属性访问逻辑。 - 改用JDK动态代理(需目标类实现接口),规避CGLIB代理对final方法的限制。
内容的提问来源于stack exchange,提问作者Zheng Xiaodong
相关产品推荐
相关产品推荐

