最近排查了一个 Spring 启动问题,异常并不陌生:
第一眼看到的时候,我的反应是:
- 三级缓存失效了?
- AOP 出问题了?
- Spring Bug?
- 代理创建顺序异常?
因为按我的理解,既然已经通过三级缓存解决了循环依赖,那么最终拿到的对象和提前暴露出去的对象应该是同一个。
结果一路跟源码,最后发现根本不是这么回事
问题现场
依赖关系非常简单:
形成循环依赖。
Spring 启动过程中成功进入三级缓存。
调试发现:
Early Reference:
最终 Bean:
明显已经被代理。
也就是说:
这正是异常的来源。
第一怀疑对象:事务代理
看到 CGLIB 的第一反应就是:
于是一路跟到:
查看:
和:
结果发现一个奇怪的现象。
整个生命周期中:
只执行了一次。
而且只发生在:
阶段。
说明:
第一条线索断掉。
如何确定是谁创建了最终代理
查看最终代理对象:
结果发现:
真凶出现了。
一个经常被忽略的代理
这个 Advisor 来自:
很多人可能没注意过它。
当 Spring 发现:
时,会自动创建异常翻译代理。
例如:
原始异常:
会被转换成:
统一 Spring 数据访问异常体系。
真正的问题
继续跟源码。
事务代理对应的:
实现了:
因此支持:
所以事务代理可以做到:
但是:
只是普通:
它根本不会参与:
只会在:
阶段执行。
于是整个流程变成:
最终:
Spring 检测到对象身份发生变化,直接抛异常。
为什么事务代理可以,异常翻译代理却不行?
跟到这里的时候,我也产生过一个疑问。
既然 Spring 已经有 Early Proxy 机制:
为什么异常翻译代理不走这一套?
继续往下看 Spring Framework 的实现后发现:
这其实是一个设计取舍。
Spring 只会为:
的代理提供 Early Proxy。
例如:
这些代理如果失效,业务逻辑会直接出错。
因此必须保证:
而:
异常翻译只是附加能力。
没有它:
一样可以正常抛出。
因此 Spring 认为没有必要为了这种场景增加整个容器复杂度。
Spring 是如何避免重复代理的
在:
中有这样一段逻辑:
随后:
如果已经在 Early 阶段创建过代理:
那么 Final 阶段就不会再次代理。
保证:
但是:
根本不参与这套机制。
因此无法保证一致性。
根因
最终定位发现:
标注的 Bean 参与了循环依赖。
循环依赖期间:
初始化完成后:
导致:
最终触发 Spring 的一致性检查。
解决方案
优先级从高到低:
方案一:拆除循环依赖(推荐)
不要形成:
这是根治方案。
方案二:非 DAO 不要滥用 @Repository
很多项目喜欢:
实际上并不访问数据库。
这种应该改成:
或者:
即可。
方案三:使用 @Lazy
避免提前创建
方案四:关闭异常翻译(一般不推荐)
移除:
或者对应自动配置。
一个容易忽略的知识点
很多人以为:
实际上并不是。
更准确的说法应该是:
对于:
如果它们只在:
阶段创建代理,
那么循环依赖场景下仍然可能出现:
从而触发 Spring 的一致性校验异常。
总结
这次问题最大的收获其实不是解决了循环依赖。
而是搞清楚了一个以前一直模糊的认知:
真正准确的说法应该是:
对于那些只在 Bean 初始化完成后才创建代理的 BeanPostProcessor:
仍然可能出现:
而这,正是那个异常背后的真正原因。