Spring Boot CGLIB代理Bean构造注入字段调用时为空问题
问题代码
SapArticleRepository实现
@Repository class SapArticleRepository(jooqDsl: DSLContext, jooqConfiguration: DefaultConfiguration) : AbstractSapRepository<TrmSapArticleRecord, TrmSapArticlePojo>(jooqDsl, jooqConfiguration)
基类AbstractSapRepository
abstract class AbstractSapRepository<R : UpdatableRecord<R?>, TPojo>( var dao: DAOImpl<R, TPojo, Long>, jooqDsl: DSLContext, jooqConfiguration: DefaultConfiguration, ) { fun findById(id: Long) = dao.findById(id) }
问题描述
运行集成测试时触发空指针异常,根源是dao字段为空。调试发现:构造函数执行期间dao是正常初始化的,但调用findById方法时dao却变成了null。确认是Spring CGLIB代理导致的——代理对象和原始对象引用不同,但搞不清楚从构造函数执行到代理Bean创建的过程中到底哪里出了问题。
试过在SapArticleRepository里实现抽象方法getDaoImpl()来返回DAO实例,通过方法访问避免空指针,但这种方式太繁琐,想找更简洁的办法,保证构造参数/字段在Bean使用时始终可用。
注:
TrmSapArticleDao是jOOQ自动生成的类。
补充:已经查过Stack Overflow相关问题,但已接受的答案解决不了我的问题。
解决方案
1. 把dao字段改为val并添加open修饰符
Spring CGLIB代理会生成目标类的子类做增强,如果字段是var且没被正确代理,很容易出现字段值丢失的情况。把字段改成不可变的val,同时加open允许代理子类访问:
abstract class AbstractSapRepository<R : UpdatableRecord<R?>, TPojo>( open val dao: DAOImpl<R, TPojo, Long>, jooqDsl: DSLContext, jooqConfiguration: DefaultConfiguration, ) { fun findById(id: Long) = dao.findById(id) }
2. 直接传入jOOQ生成的具体DAO实例
既然TrmSapArticleDao是jOOQ自动生成的,直接在子类构造时传入该DAO的实例,避免基类依赖模糊的自动注入:
@Repository class SapArticleRepository( jooqDsl: DSLContext, jooqConfiguration: DefaultConfiguration ) : AbstractSapRepository<TrmSapArticleRecord, TrmSapArticlePojo>( TrmSapArticleDao(jooqConfiguration), jooqDsl, jooqConfiguration )
3. 使用private val确保字段不可变
把dao字段改成private val,这样方法访问的是原始对象的字段,而非代理对象的同名字段,Spring代理会正确传递构造参数到原始对象:
abstract class AbstractSapRepository<R : UpdatableRecord<R?>, TPojo>( private val dao: DAOImpl<R, TPojo, Long>, jooqDsl: DSLContext, jooqConfiguration: DefaultConfiguration, ) { fun findById(id: Long) = dao.findById(id) }
背后的原因
Spring CGLIB代理通过创建目标类的子类实现功能增强,代理对象会调用原始对象的方法,但如果字段是var,代理对象的同名字段不会自动继承原始对象的初始化值——构造函数是给原始对象的字段赋值,代理对象的字段没被初始化,调用方法时用的是代理对象的字段,就会出现空指针。用val或private修饰字段,能确保方法访问的是原始对象的字段,避免代理导致的空值问题。
内容的提问来源于stack exchange,提问作者tiefenauer

