提起循环依赖,很多人自然而然想到 Spring的三层缓存,然而,Spring产生循环依赖的场景有三种:变量注入、setter注入、构造器注入,三层缓存只能解决变量注入和 setter注入。因此,今天重点分析 Spring如何处理构造器注入产生的循环依赖?
本文大纲:
- 什么是循环依赖?
- 如何解决循环依赖?
- @Lazy源码解析
- 常见面试题
申明:本文源码基于 springboot-2.7.1、spring-5.3.24 和 JDK11,版本不同,源码可能略有差异。
一、什么是循环依赖?
循环依赖是指:对象实例之间依赖关系构成一个闭环,通常分为:单个对象的自我依赖、两个对象的相互依赖、多个对象依赖成环。循环依赖抽象如下图:
单个对象的自我依赖
如下代码,在 UserService类中,通过构造器注入 UserService。
服务器启动报错,运行结果如下图:
出现这种类型的循环依赖,说明代码真的很low,要自我检讨,这里只是作为循环依赖的一种案例。
两个对象相互依赖
如下代码,UserService在类中通过构造器注入OrderService,OrderService在类中通过构造器注入UserService。
服务器启动报错,运行结果如下图:
这是一个经典的循环依赖问题,日常开发中比较常见。
多个对象的依赖成环
如下代码,UserService在类中通过构造器注入OrderService,OrderService在类中通过构造器注入GoodsService。GoodsService在类中通过构造器注入UserService。
服务器启动报错,运行结果如下图:
这种循环依赖比较隐蔽,多个对象依赖最终成环(这里以 3个对象依赖成环为例)。
通过上述的实例可以看出,对于构造器注入产生的循环依赖,服务器启动时都会报错,Spring无法自动处理。因此该如何处理循环依赖呢?
二、如何解决循环依赖?
解决构造器注入产生的循环依赖,通常有3种方式:重构代码、使用变量注入或setter注入、使用 @Lazy 注解。
2.1 重构代码
既然循环依赖是代码编写带来的,最彻底的方案是重新设计结构,消除循环依赖,但是,重构代码的范围可能不可控,因此,对于测试等存在一定的回归成本,这是一种代价稍微大点的方案。
另外,代码出现循环依赖,在一定意义上(不是绝对哦)预示了 code smell:为什么会存在循环依赖?代码抽象是否合理?代码设计是否违背了 SOLID 原则?
2.2 使用变量注入或setter注入
曾经很长一段时间,变量注入和 setter注入(Spring3.x推荐的方式)是主流的编程方式, 两种方式简单,而且,Spring 可以利用三层缓存自动解决循环依赖问题。但是,从 Spring4.x开始,官方推荐构造器注入方式,而且在 github上也可以发现大部分的开源软件都转变成构造器注入。
2.3 使用 @Lazy 注解
@Lazy 是 spring 3.0 提供的一个注解,用来表示是否要延迟初始化 bean,首先看下 @Lazy 注解的源码:
从 @Lazy 注解的源码可以总结几点:
- @Lazy 用来标识类是否需要延迟加载
- @Lazy 可以作用在类上、方法上、构造器上、方法参数上、成员变量中
- @Lazy 作用于类上时,通常与 @Component 及其衍生注解配合使用;
- @Lazy 注解作用于方法上时,通常与 @Bean 注解配合使用
因此,通过 @Lazy 解决构造器循环依赖的代码改造如下:
三、@Lazy 源码解析
本文使用 Springboot 启动,因此整体思路是:
- Springboot 是如何启动 Spring IOC 容器?
- 如何加载 Bean?
- 如何处理 @Lazy 注解?
源码查看足迹可以参考下面的类:
Springboot 启动类 main() 调用 org.springframework.boot.SpringApplication#run() org.springframework.boot.SpringApplication#refreshContext() org.springframework.context.support.AbstractApplicationContext#refresh() org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean()org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessProperties() org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance org.springframework.beans.factory.support.ConstructorResolver#autowireConstructor org.springframework.beans.factory.support.ConstructorResolver#resolvePreparedArguments org.springframework.beans.factory.support.ConstructorResolver#resolveAutowiredArgument org.springframework.beans.factory.config.AutowireCapableBeanFactory#resolveDependency()
这里摘取了处理构造器依赖的几个核心方法来解释@Lazy 如何解决循环依赖,因为 UserService 类的构造器注入 OrderService 是强依赖关系,因此会经过 AbstractAutowireCapableBeanFactory#createBeanInstance() 中关于构造器逻辑代码:
在 autowireConstructor(beanName, mbd, ctors, args) 方法会调用 ConstructorResolver#resolvePreparedArguments(),再进入 ConstructorResolver#resolveAutowiredArgument(),再进入 DefaultListableBeanFactory#resolveDependency(),resolveDependency()方法的 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary 逻辑就是针对 Lazy 情况进行处理:判断构造器参数是有@Lazy 注解,有则通过 buildLazyResolutionProxy 生成代理对象,无则直接返回 beanName。而在 buildLazyResolutionProxy()里会生成 一个 TargetSource 对象来和代理对象相关联。部分源码如下:
通过上面核心代码的解读,我们可以知道,构造器(参数)增加 @Lazy 注解后,Spring 不会去初始化参数对应类的实例,而是返回它的一个代理对象,解决了循环依赖问题,逻辑可以抽象为下图:
尽管循环依赖的问题解决了,但是,UserService 类依赖的只是 OrderService 的一个代理对象。因此,我们自然会好奇:当调用 orderService.getOrder()时,Spring 是如何找到 OrderService 的真实对象呢?
从上文知道,注入给 UserService 类的是一个代理,说起代理就不得不说起 Spring AOP 机制,它就是通过动态代理实现的(JDK 动态代理 和 CGLib 动态代理)。因为 OrderService 并非接口,因此不能使用 JDK 动态代理,只能通过 CGLib 进行代理,CGLib 源码如下:
这里抽取了 CGLib 动态代理核心的 3 步:
通过 CGLib 核心的 3 步可以解释,Spring 中代理类是如何与真实对象进行关联,因此,orderService 关联到真实对象可以抽象成下图:
另外,我们通过 3 张 IDEA debugger 截图来佐证上述理论:
到此,我们通过源码分析了 Spring是如何解决构造器循环依赖的。
四、常见面试题
Spring如何解决循环依赖?
需要从变量注入,setter注入,构造器注入 三个角度去分析,切莫笼统的说成三层缓存,同时还要注意 Spring版本,因为不同的版本,处理的方式有差异。
构造器产生的循环依赖为什么Spring无法自动处理?
这个的从类加载机制来说明,变量注入,setter注入和构造器注入的差异性。
五、总结
- Spring 构造器注入产生的循环依赖,通常可以有 3种解决办法:重构代码、变量注入或setter注入、@Lazy 注解。推荐使用 @Lazy 注解;
- @Lazy 注解解决思路是:初始化时注入代理对象,真实调用时通过 Spring AOP 动态代理去关联真实对象,然后再通过反射完成调用;
- @Lazy 注解加在构造器上,作用域为构造器所有参数,加在构造器某个参数上,作用域为该参数;
- @Lazy 注解 作用在接口上,使用 JDK 动态代理,作用在类上,使用 CGLib 动态代理;
- 日常开发,推荐使用构造器注入方式;