一次诡异的 Spring 循环依赖问题:三级缓存拿到的是原始对象,最终却变成了代理对象
最近排查了一个 Spring 启动问题,异常并不陌生:
Bean with name 'xxx' has been injected into other beans
in its raw version as part of a circular reference,
but has eventually been wrapped.
第一眼看到的时候,我的反应是:
- 三级缓存失效了?
- AOP 出问题了?
- Spring Bug?
- 代理创建顺序异常?
因为按我的理解,既然已经通过三级缓存解决了循环依赖,那么最终拿到的对象和提前暴露出去的对象应该是同一个。
结果一路跟源码,最后发现根本不是这么回事
问题现场
依赖关系非常简单:
A
↓
B
↓
Repository
↓
A
形成循环依赖。
Spring 启动过程中成功进入三级缓存。
调试发现:
Early Reference:
UserRepository
最终 Bean:
UserRepository$$SpringCGLIB$$0
明显已经被代理。
也就是说:
Early Bean != Final Bean
这正是异常的来源。
第一怀疑对象:事务代理
看到 CGLIB 的第一反应就是:
@Transactional
于是一路跟到:
AbstractAutoProxyCreator
查看:
getEarlyBeanReference()
和:
postProcessAfterInitialization()
结果发现一个奇怪的现象。
整个生命周期中:
findEligibleAdvisors()
只执行了一次。
而且只发生在:
getEarlyBeanReference()
阶段。
说明:
事务代理并不是最终代理来源
第一条线索断掉。
如何确定是谁创建了最终代理
查看最终代理对象:
Object bean = applicationContext.getBean("userRepository");
Advised advised = (Advised) bean;
for (Advisor advisor : advised.getAdvisors()) {
System.out.println(advisor);
}
结果发现:
PersistenceExceptionTranslationAdvisor
真凶出现了。
一个经常被忽略的代理
这个 Advisor 来自:
PersistenceExceptionTranslationPostProcessor
很多人可能没注意过它。
当 Spring 发现:
@Repository
时,会自动创建异常翻译代理。
例如:
@Repository
public class UserRepository {
}
原始异常:
SQLException
PersistenceException
HibernateException
会被转换成:
DataAccessException
DuplicateKeyException
统一 Spring 数据访问异常体系。
真正的问题
继续跟源码。
事务代理对应的:
AbstractAutoProxyCreator
实现了:
SmartInstantiationAwareBeanPostProcessor
因此支持:
getEarlyBeanReference()
所以事务代理可以做到:
Early Bean = Proxy
Final Bean = Proxy
但是:
PersistenceExceptionTranslationPostProcessor
只是普通:
BeanPostProcessor
它根本不会参与:
getEarlyBeanReference()
只会在:
postProcessAfterInitialization()
阶段执行。
于是整个流程变成:
Bean 创建
↓
加入三级缓存
↓
发生循环依赖
↓
获取 Early Bean
↓
拿到原始对象
↓
初始化完成
↓
PersistenceExceptionTranslationPostProcessor
创建代理
↓
Final Bean 变成代理对象
最终:
Early Bean != Final Bean
Spring 检测到对象身份发生变化,直接抛异常。
为什么事务代理可以,异常翻译代理却不行?
跟到这里的时候,我也产生过一个疑问。
既然 Spring 已经有 Early Proxy 机制:
getEarlyBeanReference()
为什么异常翻译代理不走这一套?
继续往下看 Spring Framework 的实现后发现:
这其实是一个设计取舍。
Spring 只会为:
影响 Bean 核心行为
的代理提供 Early Proxy。
例如:
@Transactional
@Aspect
@Cacheable
这些代理如果失效,业务逻辑会直接出错。
因此必须保证:
Early = Final
而:
@Repository
异常翻译只是附加能力。
没有它:
SQLException
一样可以正常抛出。
因此 Spring 认为没有必要为了这种场景增加整个容器复杂度。
Spring 是如何避免重复代理的
在:
AbstractAutoProxyCreator
中有这样一段逻辑:
public Object getEarlyBeanReference(
Object bean,
String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
随后:
public Object postProcessAfterInitialization(
Object bean,
String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
return bean;
}
如果已经在 Early 阶段创建过代理:
Early Proxy
那么 Final 阶段就不会再次代理。
保证:
Early Bean == Final Bean
但是:
PersistenceExceptionTranslationPostProcessor
根本不参与这套机制。
因此无法保证一致性。
根因
最终定位发现:
@Repository
标注的 Bean 参与了循环依赖。
循环依赖期间:
三级缓存暴露的是原始对象
初始化完成后:
PersistenceExceptionTranslationPostProcessor
再次包装代理
导致:
Raw Bean 被注入
Final Bean 变成 Proxy
最终触发 Spring 的一致性检查。
解决方案
优先级从高到低:
方案一:拆除循环依赖(推荐)
Service
↓
Repository
不要形成:
Service
↓
Repository
↓
Service
这是根治方案。
方案二:非 DAO 不要滥用 @Repository
很多项目喜欢:
@Repository
public class XxxManager {
}
实际上并不访问数据库。
这种应该改成:
@Component
或者:
@Service
即可。
方案三:使用 @Lazy
@Autowired
@Lazy
private UserRepository repository;
避免提前创建
方案四:关闭异常翻译(一般不推荐)
移除:
PersistenceExceptionTranslationPostProcessor
或者对应自动配置。
一个容易忽略的知识点
很多人以为:
Spring 三级缓存解决了循环依赖
=
所有代理都能正常工作
实际上并不是。
更准确的说法应该是:
Spring 三级缓存
只能保证支持 Early Proxy 的代理机制正常工作
对于:
PersistenceExceptionTranslationPostProcessor
MethodValidationPostProcessor
部分 Async/Scheduled 后处理器
自定义 BeanPostProcessor
如果它们只在:
postProcessAfterInitialization()
阶段创建代理,
那么循环依赖场景下仍然可能出现:
Early Bean ≠ Final Bean
从而触发 Spring 的一致性校验异常。
总结
这次问题最大的收获其实不是解决了循环依赖。
而是搞清楚了一个以前一直模糊的认知:
Spring 三级缓存
≠
解决所有循环依赖问题
真正准确的说法应该是:
Spring 三级缓存
只能解决支持 Early Proxy 的循环依赖问题
对于那些只在 Bean 初始化完成后才创建代理的 BeanPostProcessor:
postProcessAfterInitialization()
仍然可能出现:
Early Bean ≠ Final Bean
而这,正是那个异常背后的真正原因。