再谈谈 Spring 中的循环依赖
一、循环依赖
1.1、什么是循环依赖
首先,什么是循环依赖?这个其实好理解,就是两个 Bean 互相依赖,类似下面这样:
@Service
public class AService {
@Autowired
BService bService;
}
@Service
public class BService {
@Autowired
AService aService;
}
AService
和 BService
互相依赖:
这个应该很好理解。
1.2、循环依赖的类型
一般来说,循环依赖有三种不同的形态,上面 1.1 小节是其中一种。
另外两种分别是三者依赖,如下图:
这种循环依赖一般隐藏比较深,不易发觉。
还有自我依赖,如下图:
一般来说,如果我们的代码中出现了循环依赖,则说明我们的代码在设计的过程中可能存在问题,我们应该尽量避免循环依赖的发生。不过一旦发生了循环依赖,Spring 默认也帮我们处理好了,当然这并不能说明循环依赖这种代码就没问题。实际上在目前最新版的 Spring 中,循环依赖是要额外开启的,如果不额外配置,发生了循环依赖就直接报错了。
另外,Spring 并不能处理所有的循环依赖,后面会和大家进行分析。
二、循环依赖解决思路
2.1、解决思路
那么对于循环依赖该如何解决呢?其实很简单,加入一个缓存就可以了。
来看下面这张图:
如上图所示,引入了一个缓存池。
当我们需要创建 AService
的实例的时候,会首先通过 Java 反射创建出来一个原始的 AService
,这个原始 AService
可以简单理解为刚刚 new
出来(实际是刚刚通过反射创建出来)还没设置任何属性的 AService
,此时,我们把这个 AService
先存入到一个缓存池中。
接下来我们就需要给 AService
的属性设置值了,同时还要处理 AService
的依赖,这时我们发现 AService
依赖 BService
,那么就去创建 BService
对象,结果创建 BService
的时候,发现 BService
依赖 AService
,那么此时就先从缓存池中取出来 AService
先用着,然后继续 BService
创建的后续流程,直到 BService
创建完成后,将之赋值给 AService
,此时 AService
和 BService
就都创建完成了。
可能有人会说,BService
从缓存池中拿到的 AService
是一个半成品,并不是真正的最终的 AService
,但你要知道,Java 是引用传递(也可以认为是值传递,只不过这个值是内存地址),BService
当时拿到的是 AService
的引用,说白了就是一块内存地址而已,根据这个地址找到的就是 AService
,所以,后续如果 AService
创建完成后,BService
所拿到的 AService
就是完整的 AService
了。
那么上面提到的这个缓存池,在 Spring 容器中有一个专门的名字,就叫做 earlySingletonObjects
,这是 Spring 三级缓存中的二级缓存,这里保存的是刚刚通过反射创建出来的 Bean,这些 Bean 还没有经历过完整生命周期,Bean 的属性可能都还没有设置,Bean 需要的依赖都还没有注入进来。另外两级缓存分别是:
singletonObjects
:这是一级缓存,一级缓存中保存的是所有经历了完整生命周期的 Bean,即一个 Bean 从创建、到属性赋值、到各种处理器的执行等等,都经历过了,就存到singletonObjects
中,当我们需要获取一个 Bean 的时候,首先会去一级缓存中查找,当一级缓存中没有的时候,才会考虑去二级缓存。singletonFactories
:这是三级缓存。在一级缓存和二级缓存中,缓存的 key 是 beanName,缓存的 value 则是一个 Bean 对象,但是在三级缓存中,缓存的 value 是一个 Lambda 表达式,通过这个 Lambda 表达式可以创建出来目标对象的一个代理对象。
有人可能会觉得奇怪,按照上文的介绍,一级缓存和二级缓存就足以解决循环依赖了,为什么还冒出来一个三级缓存?那就得考虑 AOP 的情况了!
2.2、存在 AOP 怎么办?
上面介绍的是普通的 Bean 创建,那确实没有问题。但是 Spring 中还有一个非常重要的能力,那就是 AOP。
说到这里,先得了解一下 Spring 中 AOP 的创建流程。
正常来说是首先通过反射获取到一个 Bean 的实例,然后就是给这个 Bean 填充属性,属性填充完毕之后,接下来就是执行各种 BeanPostProcessor
了,如果这个 Bean 中有需要代理的方法,那么系统就会自动配置对应的后置处理器。
举一个简单例子,假设有如下一个 Service
:
@Service
public class UserService {
@Async
public void hello() {
System.out.println("hello>>>"+Thread.currentThread().getName());
}
}
那么系统就会自动提供一个名为 AsyncAnnotationBeanPostProcessor
的处理器,在这个处理器中,系统会生成一个代理的 UserService
对象,并用这个对象代替原本的 UserService
。
那么要搞清楚的是,原本的 UserService
和新生成的代理的 UserService
是两个不同的对象,占两块不同的内存地址!!!
再来回顾下面这张图:
如果 AService
最终是要生成一个代理对象的话,那么 AService
存到缓存池的其实还是原本的 AService
,因为此时还没到处理 AOP 那一步(要先给各个属性赋值,然后才是 AOP 处理),这就导致 BService
从缓存池里拿到的 AService
是原本的 AService
,等到 BService
创建完毕之后,AService
的属性赋值才完成,接下来在 AService
后续的创建流程中,AService
会变成了一个代理对象了,不是缓存池里的 AService
了,最终就导致 BService
所依赖的 AService
和最终创建出来的 AService
不是同一个。
为了解决这个问题,Spring 引入了三级缓存 singletonFactories
。
singletonFactories
的工作机制是这样的(假设 AService
最终是一个代理对象):
当我们创建一个 AService
的时候,通过反射刚把原始的 AService
创建出来之后,先去判断当前一级缓存中是否存在当前 Bean,如果不存在,则:
- 首先向三级缓存中添加一条记录,记录的 key 就是当前 Bean 的 beanName,value 则是一个 Lambda 表达式
ObjectFactory
,通过执行这个 Lambda 可以给当前AService
生成代理对象。 - 然后如果二级缓存中存在当前
AService
Bean,则移除掉。
现在继续去给 AService
各个属性赋值,结果发现 AService
需要 BService
,然后就去创建 BService
,创建 BService
的时候,发现 BService
又需要用到 AService
,于是就先去一级缓存中查找是否有 AService
,如果有,就使用,如果没有,则去二级缓存中查找是否有 AService
,如果有,就使用,如果没有,则去三级缓存中找出来那个 ObjectFactory
,然后执行这里的 getObject
方法,这个方法在执行的过程中,会去判断是否需要生成一个代理对象,如果需要就生成代理对象返回,如果不需要生成代理对象,则将原始对象返回即可。最后,把拿到手的对象存入到二级缓存中以备下次使用,同时删除掉三级缓存中对应的数据。这样 AService
所依赖的 BService
就创建好了。
接下来继续去完善 AService
,去执行各种后置的处理器,此时,有的后置处理器想给 AService
生成代理对象,发现 AService
已经是代理对象了,就不用生成了,直接用已有的代理对象去代替 AService
即可。
至此,AService
和 BService
都搞定。
本质上,singletonFactories
是把 AOP 的过程提前了。
2.3、小结
总的来说,Spring 解决循环依赖把握住两个关键点:
- 提前暴露:刚刚创建好的对象还没有进行任何赋值的时候,将之暴露出来放到缓存中,供其他 Bean 提前引用(二级缓存)。
- 提前 AOP:A 依赖 B 的时候,去检查是否发生了循环依赖(检查的方式就是将正在创建的 A 标记出来,然后 B 需要 A,B 去创建 A 的时候,发现 A 正在创建,就说明发生了循环依赖),如果发生了循环依赖,就提前进行 AOP 处理,处理完成后再使用(三级缓存)。
原本 AOP 这个过程是属性赋完值之后,再由各种后置处理器去处理 AOP 的(AbstractAutoProxyCreator
),但是如果发生了循环依赖,就先 AOP,然后属性赋值,最后等到后置处理器执行的时候,就不再做 AOP 的处理了。
不过需要注意,三级缓存并不能解决所有的循环依赖。
严格来说,其实也不是解决不了,所有问题都有办法解决,只是还需要额外配置。
三、特殊情况
根据前面介绍的思路,以下一些循环依赖场景无法解决。
3.1、基于构造器注入
如果依赖的对象是基于构造器注入的,那么执行的时候就会报错,代码如下:
@Service
public class AService {
BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
运行时报错如下:
Eror creating bean with name 'Aservice': Reauested bean is currently in creation: Is there an unresolvable circular reference?
原因分析:
前面我们说解决循环依赖的思路是加入缓存,如下图:
我们说先把 AService
原始对象创建出来,存入到缓存池中,然后再处理 AService
中需要注入的外部 Bean 等等,但是,如果 AService
依赖的 BService
是通过构造器注入的,那就会导致在创建 AService
原始对象的时候就需要用到 BService
,去创建 BService
时候又需要 AService
,这样就陷入到死循环了,对于这样的循环依赖执行时候就会出错。
更进一步,如果我们在 AService
中是通过 @Autowired
来注入 BService
的,那么应该是可以运行的,代码如下:
@Service
public class AService {
@Autowired
BService bService;
}
@Service
public class BService {
AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
上面这段代码,AService
的原始对象就可以顺利创建出来放到缓存池中,BService
创建所需的 AService
也就能从缓存中获取到,所以就可以执行了。
3.2、prototype 对象
循环依赖双方 scope
都是 prototype
的话,也会循环依赖失败,代码如下:
@Service
@Scope("prototype")
public class AService {
@Autowired
BService bService;
}
@Service
@Scope("prototype")
public class BService {
@Autowired
AService aService;
}
这种循环依赖运行时也会报错,报错信息如下(跟前面报错信息一样):
Eror creating bean with name 'Aservice': Reauested bean is currently in creation: Is there an unresolvable circular reference?
原因分析:
scope
为 prototype
意思就是说这个 Bean 每次需要的时候都现场创建,不用缓存里的。那么 AService
需要 BService
,所以就去现场创建 BService
,结果 BService
又需要 AService
,继续现场创建,AService
又需要 BService
…,所以最终就陷入到死循环了。
3.3、@Async
带有 @Async
注解的 Bean 产生循环依赖,代码如下:
@Service
public class AService {
@Autowired
BService bService;
@Async
public void hello() {
}
}
@Service
public class BService {
@Autowired
AService aService;
}
报错信息如下:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'AService': Requested bean is currently in creation: Is there an unresolvable circular reference?
...
2024-06-06T10:35:53.251+08:00 ERROR 14380 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| AService (field BService AService.bService)
↑ ↓
| BService (field AService BService.aService)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
其实大家从这段报错信息中也能看出来个七七八八:在 BService
中注入了 AService
的原始对象,但是 AService
在后续的处理流程中被 AOP 代理了,产生了新的对象,导致 BService
中的 AService
并不是最终的 AService
,所以就出错了!
那有人要问了,前面我们不是说了三级缓存就是为了解决 AOP 问题吗,为什么这里发生了 AOP 却无法解决?
如下两个前置知识大家先理解一下:
第一:
其实大部分的 AOP 循环依赖是没有问题的,这个 @Async
只是一个特例,特别在哪里呢?一般的 AOP 都是由 AbstractAutoProxyCreator
这个后置处理器来处理的,通过这个后置处理器生成代理对象,AbstractAutoProxyCreator
后置处理器是 SmartInstantiationAwareBeanPostProcessor
接口的子类,并且 AbstractAutoProxyCreator
后置处理器重写了 SmartInstantiationAwareBeanPostProcessor
接口的 getEarlyBeanReference
方法;而 @Async
是由 AsyncAnnotationBeanPostProcessor
来生成代理对象的,AsyncAnnotationBeanPostProcessor
也是 SmartInstantiationAwareBeanPostProcessor
的子类,但是却没有重写 getEarlyBeanReference
方法,默认情况下,getEarlyBeanReference
方法就是将传进来的 Bean 原封不动的返回去。
第二:
在 Bean 初始化的时候,Bean 创建完成后,后面会执行两个方法:
populateBean
:这个方法是用来做属性填充的。initializeBean
:这个方法是用来初始化 Bean 的实例,执行工厂回调、init
方法以及各种BeanPostProcessor
。
大家先把这两点搞清楚,然后我来跟大家说上面代码的执行流程。
- 首先
AService
初始化,初始化完成之后,存入到三级缓存中。 - 执行
populateBean
方法进行AService
的属性填充,填充时发现需要用到BService
,于是就去初始化BService
。 - 初始化
BService
发现需要用到AService
,于是就去缓存池中找,找到之后拿来用,但是!!!这里找到的AService
不是代理对象,而是原始对象。因为在三级缓存中保存的AService
的那个ObjectFactory
工厂,在对AService
进行提前 AOP 的时候,执行的是SmartInstantiationAwareBeanPostProcessor
类型的后置处理器 中的getEarlyBeanReference
方法,如果是普通的 AOP,调用getEarlyBeanReference
方法最终会触发提前 AOP,但是,这里执行的是AsyncAnnotationBeanPostProcessor
中的getEarlyBeanReference
方法,该方法只是返回了原始的 Bean,并未做任何额外处理。 - 当
BService
创建完成后,AService
继续初始化,继续执行initializeBean
方法。 - 在
initializeBean
方法中,执行其他的各种后置处理器,包括AsyncAnnotationBeanPostProcessor
,此时调用的是AsyncAnnotationBeanPostProcessor
的postProcessAfterInitialization
方法,在该方法中为AService
生成了代理对象。 - 在
initializeBean
方法执行完成之后,AService
会继续去检查最终的 Bean 是不是还是一开始的 Bean,如果不是,就去检查当前 Bean 有没有被其他 Bean 引用过,如果被引用过,就会抛出来异常,也就是上面大家看到的异常信息。
这就是本文和大家分享的三种 Spring 默认无法解决的循环依赖,其实也不是无法解决,需要一些额外配置也能解决。
那么对于以上问题该如何解决?
Spring 里边提供了办法来解决,但是似乎又没有解决,继续看你就明白了。
四、@Lazy
前面提到的三种无法自动解决的循环依赖,都可以通过添加 @Lazy
注解来解决。
如果是构造器注入,如下:
@Service
public class AService {
BService bService;
@Lazy
public AService(BService bService) {
this.bService = bService;
}
public BService getbService() {
return bService;
}
}
@Service
public class BService {
AService aService;
@Lazy
public BService(AService aService) {
this.aService = aService;
}
public AService getaService() {
return aService;
}
}
@Lazy
注解可以添加在 AService
或者 BService
的构造方法上,也可以都添加上。
添加上之后,再去启动项目,就不会报错了。
这样看起来问题解决了,但是其实还是差点意思,来看看下面的启动代码:
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
AService aService = ctx.getBean(AService.class);
BService bService = ctx.getBean(BService.class);
System.out.println("aService.getClass() = " + aService.getClass());
System.out.println("bService.getClass() = " + bService.getClass());
System.out.println("aService.getbService().getClass() = " + aService.getbService().getClass());
System.out.println("bService.getaService().getClass() = " + bService.getaService().getClass());
最终打印结果如下:
aService.getClass() = class org.javaboy.bean.aop.service.AService
bService.getClass() = class org.javaboy.bean.aop.service.BService
aService.getbService().getclass() = class org.javaboy.bean.aop.service.BService$$SpringCGLIB$$0
bService.getaService().getclass() = class org.javaboy.bean.aop.service.AService$$springCGLIB$$0
你可以看到,我们从容器中获取到的 AService
和 BService
的 Bean 都是正常的未被代理的对象,事实上我们的原始代码确实也没有需要代理的地方。但是,AService
中的 BService
以及 BService
中的 AService
却都是代理对象,按理说 AService
中的 BService
应该和我们从 Spring 容器中获取到的 BService
一致,BService
中的 AService
也应该和 Spring 容器中获取到的 AService
一致,但实际上,两者却并不相同。
不过这样也好懂了,为什么 Spring 能把一个死结给解开,就是因为 AService
和 BService
各自注入的 Bean 都不是原始的 Bean,都是一个代理的 Bean,AService
中注入的 BService
是一个代理对象,同理,BService
中注入的 AService
也是一个代理对象。
这也是为什么我一开始说这个问题 Spring 解决了又没解决。
其实,这就是 @Lazy
这个注解的工作原理,看名字,加了该注解的对象会被延迟加载,实际上被该注解标记的对象,会自动生成一个代理对象。
前面提到的另外两个问题,也可以通过 @Lazy
注解来解决,代码如下:
@Service
@Scope("prototype")
public class AService {
@Lazy
@Autowired
BService bService;
}
@Service
@Scope("prototype")
public class BService {
@Lazy
@Autowired
AService aService;
}
这里其实只要用 @Lazy
注解其中一个就能解决问题,也可以两个都添加。
对于含有 @Async
注解的情况,也可以通过 @Lazy
注解来解决:
@Service
public class AService {
@Autowired
@Lazy
BService bService;
@Async
public void hello() {
bService.hello();
}
public BService getbService() {
return bService;
}
}
@Service
public class BService {
@Autowired
AService aService;
public void hello() {
System.out.println("xxx");
}
public AService getaService() {
return aService;
}
}
如此,循环依赖的问题可以干净、彻底地解决。
总而言之一句话,@Lazy
注解是通过建立一个中间代理层,来解决循环依赖的。
4.1、原理分析
最后,来简单分析一下 @Lazy
注解处理的源码。
先来回顾一下属性注入的过程:
- 在创建 Bean 的时候,原始 Bean 创建出来之后,会调用
populateBean
方法进行 Bean 的属性填充。 - 接下来调用
postProcessAfterInstantiation
方法去判断是否需要执行后置处理器,如果不需要,就直接返回了。 - 调用
postProcessProperties
方法,去触发各种后置处理器的执行。 - 在第 3 步的方法中,调用
findAutowiringMetadata
,这个方法又会进一步触发buildAutorwiringMetadata
方法,去找到包含了@Autowired
、@Value
以及@Inject
注解的属性或者方法,并将之封装为InjectedElement
返回。 - 调用
InjectedElement#inject
方法进行属性注入。 - 接下来执行
resolvedCachedArgument
方法尝试从缓存中找到需要的 Bean 对象。 - 如果缓存中不存在,则调用
resolveFieldValue
方法去容器中找到 Bean。 - 最后调用
makeAccessible
和set
方法完成属性的赋值。
在第 7 步中,调用 resolveFieldValue
方法去解析 Bean,@Lazy
注解的相关逻辑就是在这个方法中进行处理的。
resolveFieldValue
方法最终会执行到 resolveDependency
方法:
@Nullable
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}
在这个方法中,首先会判断注入的属性类型是 Optional
、ObjectFactory
还是 JSR-330 中的注解,我们这里都不是,所以走最后一个分支。
在最后一个 else
中,首先调用 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary
方法看一下是否需要延迟加载 Bean 对象,@Lazy
注解就是在这里进行处理的。如果能够延迟加载,那么该方法的返回值就不为 null
,就可以直接返回了,就不需要执行 doResolveDependency
方法了。
ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary
:
@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}
如上所示,这个方法首先会调用 isLazy
去判断一下是否需要延迟加载,如果需要,则调用 buildLazyResolutionProxy
方法构建一个延迟加载的对象;如果不需要,则直接返回一个 null
即可。
protected boolean isLazy(DependencyDescriptor descriptor) {
for (Annotation ann : descriptor.getAnnotations()) {
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
}
return false;
}
这个判断方法主要是检查当前类中各种参数上是否含有 @Lazy
注解、方法、属性以及类名上是否含有 @Lazy
注解,如果有,则返回 true
,否则返回 false
。
再来看 buildLazyResolutionProxy
方法:
private Object buildLazyResolutionProxy(
final DependencyDescriptor descriptor, final @Nullable String beanName, boolean classOnly) {
BeanFactory beanFactory = getBeanFactory();
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
TargetSource ts = new TargetSource() {
@Override
public Class<?> getTargetClass() {
return descriptor.getDependencyType();
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() {
Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null);
Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null);
if (target == null) {
Class<?> type = getTargetClass();
if (Map.class == type) {
return Collections.emptyMap();
}
else if (List.class == type) {
return Collections.emptyList();
}
else if (Set.class == type || Collection.class == type) {
return Collections.emptySet();
}
throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
"Optional dependency not present for lazy injection point");
}
if (autowiredBeanNames != null) {
for (String autowiredBeanName : autowiredBeanNames) {
if (dlbf.containsBean(autowiredBeanName)) {
dlbf.registerDependentBean(autowiredBeanName, beanName);
}
}
}
return target;
}
@Override
public void releaseTarget(Object target) {
}
};
ProxyFactory pf = new ProxyFactory();
pf.setTargetSource(ts);
Class<?> dependencyType = descriptor.getDependencyType();
if (dependencyType.isInterface()) {
pf.addInterface(dependencyType);
}
ClassLoader classLoader = dlbf.getBeanClassLoader();
return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader));
}
这个方法就是用来生成代理的对象的,这里构建了代理对象 TargetSource
,在其 getTarget
方法中,会去执行 doResolveDependency
获取到被代理的对象,而 getTarget
方法只有在需要的时候才会被调用。所以,@Lazy
注解所做的事情,就是在给 Bean 中的各个属性注入值的时候,原本需要去 Spring 容器中找注入的对象,现在不找了,先给一个代理对象顶着,需要的时候再去 Spring 容器中查找。
五、总结
如果应用出现了循环依赖,说明设计上可能存在缺陷,类与类之间的职责界限不够清晰(是否违反了 “单一职责原则”)。尽管 Spring 提供了多种手段来解决循环依赖的问题,但还是应该尽量避免这个问题。
Ref:https://mp.weixin.qq.com/s/Sq0kt-fCsitxZvYFVNKidw