You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 03:50:26