文章目录
- 1. 循环依赖的产生
- 2. 循环依赖的解决模型
- 3. 基于setter/@Autowired 的循环依赖
- 1_编写测试代码
- 2_初始化 Cat
- 3_初始化 Person
- 4_ 回到 Cat 的创建流程
- 5_小结
- 4. 基于构造方法的循环依赖
- 5. 基于原型 Bean 的循环依赖
- 6. 引人AOP的额外设计
- 7. 总结
IOC 容器初始化bean对象的逻辑中可能会遇到bean 对象之间循环依赖的问题。Spring Framework 不提倡在实际开发中设计 bean 对象之间的循环依赖,但是当循环依赖的场景出现时IOC容器内部可以恰当地予以解决。
1. 循环依赖的产生
循环依赖,简单理解就是两个或多个bean 对象之间互相引用(互相持有对方的引用)。以下假定一个场景,人(Person)与猫(Cat)之间相互引用,人养猫,猫依赖人,用 UML 类图可以抽象为图所示。
循环依赖的产生通常与具体业务场景有关,例如在电商系统中,用户服务和订单服务之间就会存在循环依赖:用户服务需要依赖订单服务查询指定用户的订单列表,订单服务需要根据用户的详细信息对商品订单分类处理。Spring Framework 会针对不同类型的循环依赖实行不同的处理策略,下面逐个展开讲解。
2. 循环依赖的解决模型
IOC容器内部对于解决循环依赖主要使用了三级缓存的设计,其中的核心成员如下。
- singletonObjects:一级缓存,存放完全初始化好的 bean 对象容器,从这个集合中提取出来的 bean 对象可以立即返回。
- earlySingletonObjects:二级缓存,存放创建好但没有初始化属性的 bean 对象的容器,它用来解决循环依赖。
- singletonFactories:三级缓存,存放单实例 Bean工厂的容器。
- singletonsCurrentlyInCreation:存放正在被创建的 bean 对象名称的容器。
/** Cache of singleton objects: bean name –> bean instance */
private final Map singletonObjects = new ConcurrentHashMap(256);
/** Cache of singleton factories: bean name –> ObjectFactory */
private final Map> singletonFactories = new HashMap>(16);
/** Cache of early singleton objects: bean name –> bean instance */
private final Map earlySingletonObjects = new HashMap(16);
/**Names of beans that are currently in creation.**/
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16));
上述成员均在 DefaultListableBeanFactory 的父类 DefaultSingletonBeanRegistry 中。DefaultSingletonBeanRegistry 本身是一个单实例 bean 对象的管理容器,DefaultListableBeanFactory 继承它之后可以直接获得单实例 bean 对象的管理能力而无须重复设计。
3. 基于setter/@Autowired 的循环依赖
Spring Framework 可以解决的循环依赖类型是基于setter方法或 @Autowired 注解实现属性注入的循环依赖,整个 bean 对象的创建阶段和初始化阶段是分开的,这给了 IOC 容器插手处理的时机。下面通过一个具体的示例来讲解循环依赖的处理思路。
Spring单例对象的初始化大略分为三步:
- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象;
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充;
- initializeBean:调用spring xml中的init 方法。
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一步、第二步。也就是构造器循环依赖和field循环依赖。
1_编写测试代码
package org.example.bean;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class Cat {@AutowiredPerson person;
}package org.example.bean;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
public class Person {@Autowiredprivate Cat cat;
}package org.example;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class Main {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("org/example/bean"); }
}
由于 AnnotationConfigApplicationContext在创建时传入了组件扫描的根包,底层会在扫描后自动刷新IOC容器,由此就可以触发 person 与 cat 对象的初始化动作。
2_初始化 Cat
由于在字母表中 cat 比 person 的首字母靠前,IOC 容器会先初始化 cat 对象。
1、getSingleton 中的处理,方法中的关键步骤
在 bean 的创建流程中, doGetBean 和 createBean 方法(都在 AbstractBeanFactory 里实现)之间有一个特殊的步骤 beforeSingletonCreation,如代码所示(DefaultSingletonBeanRegistry.java
):
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);public Object getSingleton(String beanName, ObjectFactory<?> singleAssert.notNull(beanName, "Bean name must not be null");synchronized(this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanNameif (singletonObject == null) {//....//【控制循环依赖的关键步骤】this.beforeSingletonCreation(beanName);//......try {singletonObject = singletonFactory.getObject();newSingleton = true;} //catch finally ....// ......
beforeSingletonCreation 方法非常关键,它会检查当前正在获取的 bean 对象是否存在于 singletonsCurrentlyInCreation 集合中。如果当前 bean 对象对应的名称在该集合中已经存在,说明出现了循环依赖(同一个对象在一个创建流程中创建了两次),则抛出 BeanCurrentlyInCreationException 异常如代码所示。
protected void beforeSingletonCreation(String beanName) {if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}
}
2、对象创建完毕后的流程
第一次进人deGetBean方法汇总,此时 cat 对象对应的名称不在 singletonsCurrentlyInCreation 集合中,可以顺利进人createBean→doCreateBean 方法中,doCreateBean方法又分为3个步骤,其中第一个步骤 createBeanInstance 方法执行尝毕后,一个空的 cat 对象已经被成功创建。此时这个 cat 对象被称为“早期Bean”,而且被 BeanWrapper 包装。
接下来进入 populateBean 方法之前,源码中又设计了一个逻辑,该逻辑会提前暴露当前正在创建的 bean 对象引用,如代码所示(earySingletonExposure 的设计)。位置:AbstractAutowireCapableBeanFactory
中。
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {//.......// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// Initialize the bean instance.//......
}
注意源码中的 earlySingletonExposure
变量,它的值需要由三部分判断结果共同计算产生,包括:
- 当前创建的 bean 对象是一个单实例 bean 对象:
- IOC容器本身允许出现循环依赖(默认为
true
,在 Spring Boot 2.6.0 之后默认为false
); - 正在创建的单实例 bean 对象名称中存在当前 bean 对象的名称。
前两个条件的判断结果在当前测试场景中显然可知为 true,而第三个判断条件中,由于在上一个环节中看到 singletonsCurrentlyInCreation
集合中已经放人了当前正在创建的 “cat” 名称,因此第三个条件的判断结果也为 true。三个条件的判断结果全部为 true,所以会执行 if
结构中的 addSingletonFactory
方法。代码如下:
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}
请注意,addSingletonFactory 方法的第二个参数是一个 ObjectFactory
,在方法调用时以 Lambda 表达式传人。而方法的内部实现逻辑是将当前正在创建的 bean 对象名称保存到三级缓存 singletonFactories 中,并从二级缓存 earlySingletonObjects 中移除。此处由于二级缓存中没有正在创建的"cat"名称,因此当前环节可以简单理解为仅将cat对象的名称"cat"放人了三级缓存 singletonFactories 中。
这段代码发生在 createBeanInstance
之后,populateBean()
之前,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,此时将这个对象提前曝光出来,让大家使用。
3、依赖注入时的处理
下一个处理的时机是在 cat 对象的依赖注人时。由于使用@Autowired注解注入了Person对象,AutowiredAnnotationBeanPostProcessor 会在 postProcessProperties 方法回调时将 person 对象提取出并注入 cat 对象中。而注人的底层逻辑依然是使用 BearFactory 的 getBean 方法,如代码所示。
被依赖对象的底层获取逻辑:
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);try {metadata.inject(bean, beanName, pvs);}//catchreturn pvs;
}@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Field field = (Field) this.member;Object value;//从BeanFactory中获取被依赖对象//【此处源码有调整】 value = resolveFieldValue(field, bean, beanName);value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);//....if (value != null) {//反射注入属性ReflectionUtils.makeAccessible(field);field.set(bean, value);}
}public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {//if-else........else {//【此处源码有调整】Object result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);return result;}}public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {//try......if (instanceCandidate instanceof Class) {instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);}//...
}
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory)throws BeansException {return beanFactory.getBean(beanName);
}
当执行到最后一个方法 resolveCandidate
时,会触发 person 对象的初始化全流程。
3_初始化 Person
创建 person 对象的过程与创建 cat 类似,都是执行 getBean→doGetBean,其中包含getSingleton的处理,以及对象创建完毕后将 Person 对象包装为 ObjectFactory 后放人三级缓存 singletonFactories 中,最后到了依赖注入的环节。由于person中使用 @Autowired 注解注人了cat,因此 AutowiredAnnotationBeanPostProcessor 处理注人的逻辑与 addSingletonFactory
代码中一样,从BeanFactory中获取 cat 对象。
1、再次获取 cat 对象的细节
再次获取 cat 对象时执行的方法依然是 getBean→doGetBean,但是在 doGetBean万法中有一个非常关键的环节:getSingleton。注意方法名与上面讲解的一致,但它是另一个重载方法,如下:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);//检查当前获取的bean对象是否正在创建if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton lock//检查二级缓存中是否包含当前正在创建的bean 对象singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {//检查三级缓存中是否包含当前正在创建的bean 对象ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {//将 bean 对象放入二级缓存,并从三级缓存中移除singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}
仔细观察代码中的逻辑,IOC容器为了确保一个单实例 bean 对象不被多次创建在此处下了非常大的检查成本。
检查的范围如下:如果当前获取的 bean 对象正在创建,并且二级缓存 earlySingletonObjects中没有正在创建的 bean 对象以及三级缓存 singletonFactories 中存在正在创建的 bean 对象,说明当前获取的 bean 对象是一个没有完成依赖注入的不完全对象。即便当前 cat 是一个不完全对象,它也真实地存在于 IOC 容器中,不会影响 cat 与 person 对象之间的依赖注入(即属性成员的引用),所以 getSingleton 方法会在判断条件成立后,将当前正在获取的cat对象从三级缓存 singletonFactories中移除并放人二级缓存 earlySingletonObjects中,最后返回cat对象。
提示:注意一点,此处如果仅有一级缓存,也可以处理循环依赖,Spring Framework
在此处设计了两级缓存,是考虑到AOP的情况,AOP场景下第三级缓存singletonFactories 有特殊作用。
2、Person初始化完成后的处理
通过 getSingleton方法获取到 cat 对象后,doGetBean 方法的后续动作与循环依赖无关,此处不再提及。person 对象的 populateBean 方法执行完毕后,意味着 person 对象的属性赋值和依赖注人工作完成。后续的 initializeBean 方法中会对 person 对象进行初始化逻辑相关处理,该动作也与循环依赖无关,一并跳过。
当 doCreateBean → createBean 方法执行完毕后,回到 getSingleton 方法(最早的含有Map的代码示例)中,在方法的最后有两个关键动作,如下所示。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {//.......try {//此处的singletonractory.getobject ()即createBean 方法singletonObject = singletonFactory.getObject();newSingleton = true;}//catch......... finally {//......afterSingletonCreation(beanName);}if (newSingleton) {addSingleton(beanName, singletonObject);}}return singletonObject;}}
protected void afterSingletonCreation(String beanName) {if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {// throw ex....throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");}}
protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {this.singletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}
}
由上述源码可以发现,afterSingletonCreation 方法的作用是将当前正在获取的 bean 对象名称从 singletonsCurrentlyInCreation 中移除(移除后即代表当前环节中该 bean对象未正在创建),而 addSingleton方法的作用则是将创建好的 bean 对象放入一级缓存 singletonObjects 中,且从二级缓存 earlySingletonObjects 和三级缓singletonFactories 中移除,并记录已经创建的单实例 bean 对象。至此,一个 person 对象的创建流程完全结束。
4_ 回到 Cat 的创建流程
person 对象创建完成后,回到 cat 对象的创建流程中,此时 cat 对象的依赖注入工作尚未完成,此处会将完全创建好的 person 对象进行依赖注入。请注意,该动作完成后,即代表Cat与 Person 循环依赖的场景处理完毕。
后续的动作与Person一致,最终会将 cat 对象放入一级缓存 singletonObjects,并从其他几个缓存集合中移除,从而完成 cat 对象的创建。
5_小结
基于 setter 方法或 @Autowired属性注人的循环依赖,IOC 容器的解决流程如下。
-
创建bean 对象之前,将该 bean 对象的名称放人“正在创建的 bean 对象”集合singletonsCurrentlyInCreation中。
-
doCreateBean 方法中的 createBeanInstance 方法执行完毕后,会将当前 bean对象放入三级缓存中。注意此处放人的是经过封装后的 ObjectFactory对象,在该对象中有额外的处理逻辑。
-
对 bean 对象进行属性赋值和依赖注入时,会触发循环依赖的对象注入。
-
被循环依赖的对象创建时,会检查三级缓存中是否包含且二级缓存中不包含正在创建的、被循环依赖的对象。如果三级缓存中存在且二级缓存不存在,则会将三级缓存的 bean 对象移入二级缓存,并进行依赖注如。
-
被循环依赖的 bean 对象创建完毕后,会将该 bean 对象放入一级缓存,并从其他缓存中移除。
-
所有循环依赖的 bean 对象均注入完毕后,一个循环依赖的处理流程结束。
4. 基于构造方法的循环依赖
对于基于构造方法的循环依赖场景,IOC 容器无法给予合理的处理,只能抛出 BeanCurrentlyInCreationException 异常,原因是通过构造方法进行循环依赖,在构造方法没有执行完毕时,bean 对象尚未真正创建完毕并返回,此时若不加干预,会导致参与循环依赖的对象产生构造方法循环调用闭环,从而一直在轮流创建对象,直至内存溢出。IOC容器为了避免对象的无限创建,采用 singletonsCurrentlyInCreation
集合记录正在创建的 bean 对象名称,当一个 bean 对象名称出现两次时,IOC 容器会认为出现了不可解决的循环依赖从而抛出 BeanCurrentlyIncreationException 异常。下面通过一个例子简要分析,代码如下所示。
@Component
public class Cat {public Cat(Person person) {this.person = person;}Person person;
}@Component
public class Person {public Person(Cat cat) {this.cat = cat;}private Cat cat;
}
通过代码清单中的两个循环依赖的类,可以推演以下步骤。
- IOC容器首先创建 cat 对象,由于调用 cat 的构造方法需要依赖 person 对象,从而引发 person 对象的创建。
- IOC容器创建 person对象,由于调用 person 的构造方法需要依赖 cat 对象,从而引发 cat 对象的创建。
- IOC 容器第二次创建 cat 对象,由于第一次创建 cat 对象时在
singletonsCurrentlyInCreation
集合中存放了"cat"的名称,因此当第二次创建cat 对象时,singletonsCurrentlyInCreation 集合中已存在"cat"名称,从而抛出 BeanCurrentlyInCreationException 异常,表示出现了不可解决的循环依赖。
5. 基于原型 Bean 的循环依赖
对于基于原型 Bean 之间循环依赖的场景,IOC 容器也无法合理解决,因为 IOC 容器不会对原型 Bean 进行缓存,只会像记录单实例 Bean 的创建时那样记录正在创建的 bean 对象名称。这种设计会导致即使原型 bean 对象已经实例化完毕,也无法通过有效手段将该 bean 对象的引用暴露,从而引发原型 bean 对象的无限创建。以下是一个原型 Bean 场景的推演,测试代码可以选择上面的代码,只需给两个类声明 @Scope("prototype")
注解。
- IOC容器首先创建 cat 对象,之后进行 person 对象的依赖注入,由于 person 被定义为原型 Bean,触发 person 对象的创建。
- IOC容器创建 person 对象,之后进行cat 对象的依赖注入,由于 cat 对象也被定义为原型 Bean,触发 cat 对象的全新创建。
- IOC 容器再次创建 cat 对象,由于第一次创建 cat对象时
prototypesCurrentlyInCreation
原型 bean 对象名称集合(注意与singletonsCurrentlyInCreation
集合区分)中已经存放了"cat"名称,因此当第二次创建时,prototypesCurrentlyInCreation 集合中已存在"cat"名称,从而抛出 BeanCurrentlyIncreationException 异常,表示出现了不可解决的循环依赖。
6. 引人AOP的额外设计
对于在 3.3.1 节中提到的 getSingleton方法中将三级缓存中的 bean 对象放入二级缓存的动作,其实如果仔细观察会发现,三级缓存中存放的是被封装过的 ObjectFactory 对象,而二级缓存中存放的是真正的 bean 对象,为什么会有 ObjectFactory 到 bean 对象之间的过渡呢 ?这就是 Spring Framework设计的高深之处。Spring Framework的两大核心特性中,除 IOC 之外还有一个重要特性是 AOP,在 bean对象创建完成后,IOC 容器会指派BeanPostProcessor 对需要进行 AOP 增强的 bean 对象进行代理对象的创建。原始的目标对象和被 AOP 增强的代理对象本质上是两个完全不同的对象,IOC 容器为了确保 bean 对象中最终注入的是 AOP 增强后的代理对象而不是原始对象,会在 ObjectFactory 到 bean 对象的过渡期间进行额外的检查,该环节的检查会提前创建代理对象,并替换原始对象。经过此法处理后的 bean 对象就是一个被 AOP 增强后的代理对象,即便后续执行属性赋值和依赖注入,最终也是给内层的目标对象赋值和注入,而不会有任何副作用,但是从 IOC 容器的整体角度而言,IOC 容器内部的所有 bean 对象通过依赖注入后的属性成员都是正确的 bean 对象(此处的正确是指如果一个 bean 对象的确需要被 AOP 增强,则注人的是正确的代理对象而不是错误的原始对象 )。
下面从源码的角度简单了解引入 AOP 之后的额外逻辑触发。通过下列代码可以发现 getEarlyBeanReference 方法的实现是回调 IOC 容器中所有SmartInstantiationAwareBeanPostProcessor
的 getEarlyBeanReference
方法,该方法可以获得单实例 bean 对象的引用,也正是通过该方法 IOC容器可以有机会将一个普通 bean 对象转化为被 AOP 增强的代理对象。
代码 getEarlyBeanReference
方法的实现:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {//.......// Eagerly cache singletons to be able to resolve circular references// even when triggered by lifecycle interfaces like BeanFactoryAware.boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) { //此处调用addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}//.........
}protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);}}return exposedObject;
}
所有实现 AOP 增强的后置处理器都继承自AbstractAutoProxyCreator
,而它本身实现了 SmartInstantiationAwareBeanPostProcessor 接口,内部自然有 getEarlyBeanReference 方法的实现了。如下所示。
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = this.getCacheKey(bean.getClass(), beanName);this.earlyProxyReferences.put(cacheKey, bean);//必要时创建代理对象return this.wrapIfNecessary(bean, beanName, cacheKey);
}
由 AbstractAutoProxyCreator 实现的逻辑可以明显看出,如果当前正在创建的 bean 对象的确需要创建代理对象(即有必要),则会先行创建代理对象,并替换原始对象。由此就解释了为什么 IOC 容器解决循环依赖需要使用三级缓存而不是二级。
至此,IOC 容器解决循环依赖的方案全部讲解完毕,读者最好能自行编写一些实际的测试代码,配合 Debug 体会一遍,以加深印象。
7. 总结
Spring 的循环依赖存在三种情况:
- 构造器的循环依赖
- 单例模式下的 setter /Autowired循环依赖
- 非单例循环依赖
Spring 通过提前暴露单例 bean的机制来处理属性注入中的循环依赖。对于构造器注入、非单例循环依赖,Spring 无法解决循环依赖问题,需要开发者注意设计避免循环依赖的情况。
实现原理是采用三级缓存保存提前暴露的单例 bean,且用 Set集合记录正在创建中的 bean 对象来判断是否产生了循环依赖。
需要理解三级缓存中各自的作用即工作流程。
Spring 通过这一系列策略确保了在大多数情况下能够正确地解决循环依赖问题,保证了容器的稳定性和一致性。
另外,@Lazy
采用延迟加载的方式也可以解决循环依赖的问题。但这些都是治标不治本,辜负了SpringBoot的一片苦心。
项目中存在 Bean 的循环依赖,其实是代码质量低下的表现。多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误。
还好,SpringBoot 终于受不了这种滥用,默认把循环依赖给禁用了!