前言
最近遇到了这样的状况,项目在本地、开发、测试都正常启动,到了生产就出现了因为循环依赖导致项目启动不了的问题,原因还是项目里面字段注入和构造器注入混用,在生产环境构造器注入的Bean先创建,导致Spring无法解决这样的循环依赖。其实这个问题在《记一次排查循环依赖的经历》已经讲过一遍了,在这篇文章里我们不断地往下追源码,试图找出什么会影响Bean的创建顺序。
在《记一次排查循环依赖的经历》这篇文章里面,我们通过改类的名字来改变加载顺序来验证Bean的创建顺序对Spring 的影响,这是一种验证思路,但是分析的还不够彻底。本篇我们换一种方式来干扰Bean的创建顺序来验证我们的思想。在探究循环依赖的过程,我发现循环依赖其实是图的结构,@Aysnc也会对循环依赖产生影响,死锁也和循环依赖具备结构上的相似性。然后我们能否引入检测组件,提前检测这种被环境影响的循环依赖。
围绕着这些问题,写了一系列的文章,初步有以下几篇:
- 图与循环依赖、死锁(一):为何我的循环依赖时好时坏?
- 图与循环依赖、死锁(二):@Async对循环依赖的影响?
- 图与循环依赖、死锁(三):三级缓存的引入动机
- 图与循环依赖、死锁(四): 如何在启动前就发现循环依赖?
- 图与循环依赖、死锁(五):死锁
循环依赖的第一种形式
那首先什么是循环依赖 ,在Java里面,设A有成员变量B,B里面有成员变量A,那么这就是循环依赖:
typescript
public class BCycle {
private ACycle aCycle;
public ACycle getaCycle() {
return aCycle;
}
public void setaCycle(ACycle aCycle) {
this.aCycle = aCycle;
}
}
public class ACycle {
private BCycle bCycle;
public BCycle getbCycle() {
return bCycle;
}
public void setbCycle(BCycle bCycle) {
this.bCycle = bCycle;
}
}
这其实就是循环依赖,但这样其实也没有什么问题 , 我们创建ACycle实例和BCycle的时候 , 我们可以这么写:
ini
ACycle a = new ACycle();
BCycle b = new BCycle();
a.setbCycle(b);
b.setaCycle(a);
这其实道出了解决循环依赖的精髓所在,当两个Bean互相依赖的时候,先用无参的构造函数,创造出来一个对象,做属性填充。如果你不想自己手动new,我们可以用反射来做,首先我们需要标记哪些对象需要new,其次我们希望知道对象所属类的成员变量,哪些是需要填充的,因此我们需要将这些数据让程序识别到,或者xml,或者用注解打上标记。现在我们选择用注解打上标记,来在运行时创造出这些对象。考虑最简单的情况,假设A不依赖任何类,但是B依赖A,像下面这样:
kotlin
public class A{
}
public class B{
private A a;
}
在创建B实例的时候,就需要给它的成员变量a填入值,因此在我们自动创建对象的过程中需要一个Map,存储这些自动创建的对象,key就是类名小写。所以我们的程序可以这么写。首先我们需要一些注解来给标识类文件和成员变量:
less
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE
})
public @interface AutoCreateObject {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,
ElementType.METHOD,
})
public @interface AutoFillValue {
}
然后将这两个注解打到类上:
typescript
@AutoCreateObject
public class ACycle {
@AutoFillValue
private BCycle bCycle;
public BCycle getbCycle() {
return bCycle;
}
public void setbCycle(BCycle bCycle) {
this.bCycle = bCycle;
}
}
@AutoCreateObject
public class BCycle {
@AutoFillValue
private ACycle aCycle;
public ACycle getaCycle() {
return aCycle;
}
public void setaCycle(ACycle aCycle) {
this.aCycle = aCycle;
}
}
然后我们可以写自动创建对象了:
scss
public class MockSpring {
private final static Map<String,Object> stringObjectMap = new ConcurrentHashMap<>();
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 其实我们也考虑 扫描当前类所在包下面的类或者子包
// 但我们这里只是为了演示同反射自动创建对象
List<Class> classList = new ArrayList<>();
classList.add(ACycle.class);
classList.add(BCycle.class);
for (Class aClass : classList) {
// 此方法自Class类的newInstance被废弃
Object instance = aClass.newInstance();
stringObjectMap.put(aClass.getName(),instance);
Field field = needAutowire(aClass);
// 说明这个字段要自动装配
if (field != null){
Class<?> needAutoClazz = field.getType();
Object instance2 = needAutoClazz.newInstance();
field.setAccessible(true);
field.set(instance,instance2);
stringObjectMap.put(needAutoClazz.getName(),instance2);
Field clazzField = needAutowire(needAutoClazz);
Object object = stringObjectMap.get(clazzField.getType().getName());
if (object != null){
clazzField.setAccessible(true);
clazzField.set(instance2,instance);
}
}
}
ACycle aCycle = getTargetBean(ACycle.class);
System.out.println(aCycle.getbCycle());
BCycle bCycle = getTargetBean(BCycle.class);
System.out.println(bCycle.getaCycle());
}
private static <T> T getTargetBean(Class<T> tClass) {
return (T) stringObjectMap.get(tClass.getName());
}
private static Field needAutowire(Class aClass) {
Field[] declaredFields = aClass.getDeclaredFields();
// 获取所有的字段
for (Field declaredField : declaredFields) {
for (Annotation annotation : declaredField.getDeclaredAnnotations()) {
if (annotation instanceof AutoFillValue) {
return declaredField;
}
}
}
return null;
}
}
注意看我们仅仅用了一级缓存就解决了循环依赖,粗略的说缓存就是存储在内存的数据。我们目前的框架考虑都只是最简单的对象创建场景, 并没有考虑setter注入,构造器注入等其他情况。尤其是构造器注入,Spring官方推荐构造器注入,构造器注入可以让我们意识到这个类依赖了多少变量,如果你看到一个类依赖了太多变量,那么这个类就不是那么职责单一。 我们不在这篇文章里面讨论这个内容,有兴致的话参看《从 NPE 到高内聚:Spring 构造器注入的真正价值》,在这一篇对为什么推荐构造器注入做了细致的讨论。
循环依赖的第二种形式
现在让我们用循环依赖变一种形式:
typescript
public class A01Service {
private A02Service a02Service;
public A01Service(A02Service a02Service) {
this.a02Service = a02Service;
}
}
public class A02Service {
private A01Service a01Service;
public void setA01Service(A01Service a01Service) {
this.a01Service = a01Service;
}
}
我们肯定不会写出下面的写法:
ini
A01Service a = new A01Service();
A02Service b = new A02Service();
b.setA01Service(a);
这样会编译不过去,我们事实上可以这么写:
ini
A02Service b = new A02Service();
A01Service a = new A01Service(b);
b.setA01Service(a);
上面的循环依赖其实描述的就是Bean创建顺序对循环依赖问题的影响,如果是A01Service先创建,因为A01Service先创建,需要一个A02Service。A02Service需要a, 因为A01Service里面没有一个无参构造函数,创造不出来一个未初始化完成的Bean。所以如果构造函数在前的Bean先创建,则此循环依赖无法被解决。
由此就引出了Bean的顺序性对循环依赖的影响。
顺序对于 Spring解决循环依赖的影响
上面我们做出了论断,Bean的顺序性对Spring解决循环依赖有一定的影响,假设A和B互相循环依赖,如果A里面的是构造器注入,B是字段注入。则此循环依赖无法被Spring解决。这是我们对理论的预测,现在我们基于理论做验证,我们有两种思路,一种是看什么影响Bean的加载顺序,一路往下追。这就是《记一次排查循环依赖的经历》的思路,再有一种思路就是, 我们知道Spring在启动的时候会扫描Bean的定义,也就是BeanDefinition,将其放入到集合里面。然后遍历这个集合开始创建BeanDefinition。
Spring 创建Bean概述
在Spring Boot 下面@ComponentScan这个注解默认扫描,主程序所在的类所在的包的类及其子包的类。我们知道注解是不直接发挥作用的,这个注解由对应的ComponentScanAnnotationParser来发挥对应的作用,ComponentScanAnnotationParser 主要将Bean的相关定义扫描并放入到一个集合里面。
然后在SpringApplication的run方法来触发Bean的初始化逻辑,最后是在finishBeanFactoryInitialization里面开始创建Bean:
scss
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}
beanFactory.setTempClassLoader(null);
beanFactory.freezeConfiguration();
beanFactory.preInstantiateSingletons();
}
这里的beanFactory事实上DefaultListableBeanFactory,里面的preInstantiateSingletons逻辑为:
scss
@Override
public void preInstantiateSingletons() throws BeansException {
if (logger.isTraceEnabled()) {
logger.trace("Pre-instantiating singletons in " + this);
}
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
// Trigger initialization of all non-lazy singleton beans...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
if (bean instanceof SmartFactoryBean<?> smartFactoryBean && smartFactoryBean.isEagerInit()) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
}
// Trigger post-initialization callback for all applicable beans...
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
StartupStep smartInitialize = getApplicationStartup().start("spring.beans.smart-initialize")
.tag("beanName", beanName);
smartSingleton.afterSingletonsInstantiated();
smartInitialize.end();
}
}
}
也就是说我们只用重排DefaultListableBeanFactory中this.beanDefinitionNames的顺序即可。现在思路有了,我们该怎么在Spring Boot 启动的时候拿到DefaultListableBeanFactory的实例,进而改写this.beanDefinitionNames的顺序呢?
干扰Bean的创建顺序
首先我们准备两个Bean:
kotlin
@Component
public class A01Service {
private A02Service a02Service;
public A01Service(A02Service a02Service) {
this.a02Service = a02Service;
}
}
@Component
public class A02Service {
@Autowired
private A01Service a01Service;
}
在这种情况下Spring 是无法解决这个循环依赖的, 会启动失败。我们通过实现BeanFactoryPostProcessor来改写这个Bean的创建顺序,将A02Service的创建顺序提前:
php
@Component
public class OrderBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
private static final VarHandle METHOD_HANDLE;
static {
try {
METHOD_HANDLE = MethodHandles.privateLookupIn(DefaultListableBeanFactory.class,MethodHandles.lookup())
.findVarHandle(DefaultListableBeanFactory.class,
"beanDefinitionNames",List.class);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
try {
// 注意这是JDK 17的语法
// 模式匹配
if (beanFactory instanceof DefaultListableBeanFactory){
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
for (int i = 0; i < beanDefinitionNames.length; i++) {
if ("a01Service".equals(beanDefinitionNames[i])){
beanDefinitionNames[i] = "a02Service";
}else if ("a02Service".equals(beanDefinitionNames[i])){
beanDefinitionNames[i] = "a01Service";
}
}
METHOD_HANDLE.set(beanFactory, Arrays.asList(beanDefinitionNames));
}
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
会发现我们调整过后,原先启动不起来的项目能启动起来了。这证明了我们上面的论断,顺序是影响循环依赖的。这就意味着在不同的环境下面,读到的Bean的顺序是不确定的,我们始终要调操作系统的接口去读包下面的类,但是不同操作系统下面读取文件下面的顺序是不同的,因此我们在开发环境跑不出来的循环依赖影响启动,到其他环境就可能跑出来。
那怎么解决循环依赖

从循环依赖的结构图来说,循环依赖就是对于两个Bean来说,两个Bean互相要求注入, 如果都是字段注入,Spring可以临时先new 一个空的Bean先注入,打断环。但是只要依赖构造器注入的Bean先走创建Bean的流程,创建A发现依赖B,转而去创建B,在创建B的时候依赖A,在A里面找不到一个无参的构造函数。于是启动失败。那该怎么解决循环依赖呢? 这里的解决分为两种语义,第一种解决是循环依赖在,但是Spring能正常启动,将不能解决的循环依赖改为Spring能解决的循环依赖。这种是缓解循环依赖。第二种是彻底解决循环依赖,移除一条依赖回路。
构造器注入改为字段注入
我们解决这种循环依赖的时候,目前直接的思路就是将其改为字段注入,字段注入的Spring能解决,改动也最小:
kotlin
@Component
public class A01Service {
@Autowired
private A02Service a02Service;
private final A03Service a03Service;
public A01Service(A03Service a03Service) {
this.a03Service = a03Service;
}
}
@Component
public class A02Service {
@Autowired
private A01Service a01Service;
}
@Component
public class A03Service {
}
懒加载
我们注意观察假设两个Bean是循环依赖,姑且命名为A和B,在创建A的时候,发现依赖B,B要求A,但是这个时候回到A的时候又需要B。这样就无法打破环。我们可以不要求另一方在启动的时候去完全创建, 也就是将Bean的创建时期拖到了运行时:
typescript
@Component
public class A01Service {
private A02Service a02Service;
public A01Service(A02Service a02Service) {
this.a02Service = a02Service;
}
public void sayA01(){
System.out.println("sayA01");
}
}
@Component
public class A02Service {
@Autowired
@Lazy
private A01Service a01Service;
public void sayA02(){
System.out.println("sayA02");
}
}
我们在A02Service上的a01Service字段打上了@Lazy注解,就是告诉Spring在创建A02Service,填充a01Service可以延迟执行。这样同样也可以让Spring启动起来。
引入中间层
上面我们都着眼于将Spring 不能解决的循环依赖变成Spring能解决的循环依赖,另一种彻底解决循环依赖的思路,就是让代码不再有循环依赖。我们不妨将两个依赖的Bean,设为A和B,A里面依赖B,B依赖A,我们将B依赖A的方法移动到一个全新的类里面变成C,C依赖A。那B原先依赖A里面的方法该怎么办,我们可以通过事件发布订阅转发:
typescript
@Component
public class A01Service {
private A02Service a02Service;
public A01Service(A02Service a02Service) {
this.a02Service = a02Service;
}
public void sayA01(String args){
System.out.println(args);
}
}
@Component
public class A01ServiceServiceListener {
private A01Service a01Service;
public A01ServiceServiceListener(A01Service a01Service) {
this.a01Service = a01Service;
}
@EventListener(A01Event.class)
public void dispatchA(A01Event a01Event) {
a01Service.sayA01(a01Event.getArgs());
}
}
@Component
public class A01Service {
private A02Service a02Service;
public A01Service(A02Service a02Service) {
this.a02Service = a02Service;
}
public void sayA01(String args){
System.out.println(args);
}
}
总结一下
本篇从循环依赖入手,主要讲什么是循环依赖,对任意两个Bean里面,互相要求注入。即为循环依赖。默认情况下,如果你不做特殊声明,Spring要求在启动的时候填充所有需要依赖注入,如果是字段级别的注入,Spring大多数情况下是可以解决循环依赖问题,注意这个大多数,我们会在下一篇文章中解释,即使是字段级别的注入,在某些情况下Spring 仍然是无法解决循环依赖的。
我们本篇介绍了Bean的创建顺序对循环依赖的影响,也就是说循环依赖中,存在一方为构造器注入,一方为字段构造器注入,那么如果构造器注入的Bean先创建,那么这种循环依赖同样是无法解决的,因为在创造字段构造器注入的Bean的时候,无法创建构造器注入的Bean。而Spring要求启动之后,所有的Bean属性填充完成。我们还讲了如何解决循环依赖,第一种是缓解,让项目正常启动的,方式是将构造器注入的字段转为字段级别的注入,或者延迟加载。第二种是打断依赖回路,设A依赖B,调用了B中的若干方法, 我们可以通过事件监听,将原先的调用通过事件监听订阅转发到B上面。