图与循环依赖、死锁(一) 为何我的循环依赖时好时坏?

前言

最近遇到了这样的状况,项目在本地、开发、测试都正常启动,到了生产就出现了因为循环依赖导致项目启动不了的问题,原因还是项目里面字段注入和构造器注入混用,在生产环境构造器注入的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上面。

相关推荐
元闰子3 分钟前
分离还是统一,这是个问题
后端·面试·架构
寻月隐君12 分钟前
Rust Scoped Threads 实战:更安全、更简洁的并发编程
后端·rust·github
爷_1 小时前
手把手教程:用腾讯云新平台搞定专属开发环境,永久免费薅羊毛!
前端·后端·架构
山间小僧1 小时前
「查漏补缺」ZGC相关内容整理
java·jvm·后端
程序视点2 小时前
FDM下载神器:免费多线程下载工具,速度90+M/S,完美替代1DM!
windows·后端
happycode7002 小时前
数据库迁移实践
后端
Livingbody2 小时前
【ERNIEKit】基于ERNIE4.5-0.3B大模型微调的心理咨询师大模型全流程
后端
CRUD被占用了3 小时前
coze-studio学习笔记(二)
后端
codervibe3 小时前
Spring Boot 项目中泛型统一响应封装的设计与实现
后端