【Spring系列笔记】依赖注入,循环依赖以及三级缓存

132 阅读10分钟

1. 依赖注入方式

1.1. 构造器注入

1.1.1. 概述

构造器注入是指通过构造方法将依赖项注入到对象中。在构造方法中,将依赖项作为参数传入,然后在对象被创建时将其保存在成员变量中。
构造器注入是一种简单有效的依赖注入方式,可以保证依赖项的不可变性。在实际开发中,如果依赖项是必需的,且不需要在对象生命周期内发生变化,可以考虑使用构造器注入。


@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository.getUserById(id);
    }
}

1.1.2. 特点

构造方法注入是 Spring 官方从 4.x 之后推荐的注入方式。

在Spring 4.3 以后,如果我们的类中只有单个构造函数 ,不写 @Autowired注解也可实现依赖注入。这种注入称为隐式注入

优点

  1. 可注入不可变对象;注入时对象可用final修饰。在 Java 中 final 对象(不可变)要么直接赋值,要么在构造方法中赋值,所以当使用set方法注入或注解注入 final 对象时,它不符合 Java 中 final 的使用规范,所以就不能注入成功了。
  2. 注入对象不会被修改;构造方法在对象创建时只会执行一次,因此它不存在注入对象被随时(调用)修改的情况。
  3. 注入对象会被完全初始化;构造方法是在对象创建之初执行的,因此被注入的对象在使用之前,会被完全初始化 。
  4. 通用性更好,适用于 IoC 框架还是非 IoC 框架 。
  5. 固定依赖注入的顺序,避免循环依赖的问题。

缺点

  1. 代码臃肿,可读性差,不便维护。

1.2. setter方法注入

1.2.1. 概述

Setter方法注入是指通过setter方法将依赖项注入到对象中。在setter方法中,将依赖项作为参数传入,然后将其保存在成员变量中。
Setter方法注入是一种常用的依赖注入方式,可以保证依赖项的可变性。在实际开发中,如果依赖项可能发生变化,或者是可选的,可以考虑使用Setter方法注入。

public class UserService {
    private UserRepository userRepository;

    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository.getUserById(id);
    }
}

1.2.2. 特点

在Spring 3.x 中,Spring建议使用setter来注入

优点

  1. 完全符合单一职责的设计原则
  2. 只有对象是需要时才会注入依赖,而不是在初始化的时候就注入。
  3. 依赖的可变性。

缺点

  1. 无法注入一个不可变的对象;

1.3. 接口注入

1.3.1. 概述

接口注入是指通过实现接口将依赖项注入到对象中。在接口中定义依赖项的setter方法,然后在实现类中实现该方法,将依赖项注入到对象中。
接口注入相对于构造方法注入和Setter方法注入,需要定义额外的接口,增加了代码复杂度,但可以保证依赖项的可变性。

public interface UserRepositorySetter {
    void setUserRepository(UserRepository userRepository);
}


public class UserService implements UserRepositorySetter {
    private UserRepository userRepository;

    @Override
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository.getUserById(id);
    }
}

1.4. 注解注入

1.4.1. 概述

注解注入是指通过注解将依赖项注入到对象中。在依赖项上添加注解,然后在对象中使用@Autowired注解将依赖项注入到对象中。
注解注入是一种简单便捷的依赖注入方式,可以保证依赖项的可变性。在实际开发中,如果使用Spring等框架,可以考虑使用注解注入。

public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User getUserById(int id) {
        return userRepository.getUserById(id);
    }
}

1.4.2. 特点

优点

  1. 实现简单,使用简单,方便维护

缺点

  1. 功能性问题:无法注入一个不可变的对象;
  2. 通用性问题:只能适应于 IoC 容器;
  3. 设计原则问题:更容易违背单一设计原则。

2. 循环依赖

2.1. 什么是循环依赖?

Spring 循环依赖是指:两个或多个不同的 Bean 对象,相互成为各自的字段,当这两个 Bean 中的其中一个 Bean 进行依赖注入时,会陷入死循环,即循环依赖现象。

@Component
public class UserServiceA {
    @Autowire
    private UserServiceB userServiceB;
}

@Component
public class UserServiceB {
    @Autowire
    private UserServiceA userServiceA;
}

2.2. 循环依赖会出现什么问题?

在没有考虑Spring框架的情况下,循环依赖并不会带来问题,因为对象之间相互依赖是非常普遍且正常的现象。但使用Spring框架,我们将创建Bean对象的控制权交给容器,当出现循环依赖时,容器会不知道先创建哪个Bean,会爆异常 BeanCurrentlyInCreationException 。

在Spring框架中,一个对象的实例化并非简单地通过new关键字完成,而是经历了一系列Bean生命周期的阶段。正是由于这种Bean的生命周期机制,才导致了循环依赖问题的出现。要深入理解Spring中的循环依赖,首先需要对Spring中Bean的完整生命周期有所了解。

2.3. Bean生命周期

Spring 管理的对象称为 Bean,通过Spring的扫描机制获取到类的BeanDefinition后,接下来的流程是:

  1. 解析BeanDefinition以实例化Bean:
  • 推断类的构造方法。
  • 利用反射机制实例化对象(称为原始对象)。
  1. 填充原始对象的属性,实现依赖注入。
  2. 如果原始对象中的方法被AOP增强,CGLIB动态代理继承原始对象生成代理对象。
  3. 将生成的代理对象存放到单例池(在源码中称为singletonObjects)中,以便下次直接获取。

这个过程简要描述了Spring容器在实例化Bean并处理AOP时的流程。

在Spring中,Bean的生成过程涉及多个复杂步骤,远不止上述简要提及的4个步骤。除了所列步骤外,还包括诸如Aware回调、初始化等繁琐流程。

2.4. 代码层面实现

2.4.1. 初始定义

定义一个学生类以及教师类

/**
* 定义一个Teacher对象,并交给IOC管理
*/ 
@Component 
@Data
public class Teacher {
    //老师姓名
    private String name;
    //注入关联Student对象@Autowired
    private Student student;
}

/**
* 定义一个学生对象,并交给IOC管理
*/ 
@Component 
@Data
public class Student {
    //学生姓名
    private String name;
    //注入关联Teacher对象@Autowired
    private Teacher teacher;
}

2.4.2. 配置解决

从SpringBoot2.6.0以后的版本开始,SpringBoot默认不会自动解决set方式循环依赖问

题,如果要解决我们需要在application.yml中添加配置解决循环依赖

spring:
  main:
    #允许spring中利用set方式解决自动循环依赖问题
    allow-circular-references: true

2.4.3. 在构造方法上添加@Lazy

思路:打破循环依赖只需让一个对象实例先初始化完成

/**
* 定义一个Teacher对象,并交给IOC管理
*/ 
@Component 
@Data
public class Teacher {
    //老师姓名
    private String name;
    //注入关联Student对象@Autowired
    private Student student;

    public Teacher(Student student){
        this.student = student;
    }
    
}

/**
* 定义一个学生对象,并交给IOC管理
*/ 
@Component 
@Data
public class Student {
    //学生姓名
    private String name;
    //注入关联Teacher对象@Autowired
    private Teacher teacher;

    public Student(@Lazy Teacher teacher){
        this.teacher = teacher;
    }
    
}

3. 三级缓存

3.1. 概述

而针对循环依赖,Spring通过一些机制来协助开发者解决部分循环依赖问题,这便是三级缓存。

SingletonObjects一级缓存存储完整的 Bean;
EarlySingletonObjects二级缓存存储从第三级缓存中创建出代理对象的 Bean,即半成品的 Bean;
SingletonFactory三级缓存存储实例化完后,包装在 FactoryBean 中的工厂 Bean;
public class DefaultSingletonBeanRegistry implements SingletonBeanRegistry {
​
    /**
     * 一级缓存
     */
    private Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
​
    /**
     * 二级缓存
     */
    private Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
​
    /**
     * 三级缓存
     */
    private Map<String, ObjectFactory<?>> singletonFactory = new HashMap<>();
​
    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // Quick check for existing instance without full singleton lock
        Object singletonObject = this.singletonObjects.get(beanName);
        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
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }
}

在上面的 getSingleton 方法中,先从 SingletonObjects 中获取完整的 Bean,如果获取失败,就从 EarlySingletonObjects 中获取半成品的 Bean,如果 EarlySingletonObjects 中也没有获取到,那么就从 SingletonFactory 中,通过 FactoryBean 的 getBean 方法,获取提前创建 Bean。如果 SingletonFactory 中也没有获取到,就去执行创建 Bean 的方法。

3.2. 解决循环依赖

Spring 产生一个完整的 Bean 可以看作三个阶段:

  • createBean:实例化 Bean;
  • populateBean:对 Bean 进行依赖注入;
  • initializeBean:执行 Bean 的初始化方法;

产生循环依赖的根本原因是:对于一个实例化后的 Bean,当它进行依赖注入时,会去创建它所依赖的 Bean,但此时它本身没有缓存起来,如果其他的 Bean 也依赖于它自己,那么就会创建新的 Bean,陷入了循环依赖的问题。

所以,三级缓存解决循环依赖的根本途径是:当 Bean 实例化后,先将自己存起来,如果其他 Bean 用到自己,就先从缓存中拿,不用去创建新的 Bean 了,也就不会产生循环依赖的问题了。过程如下图所示:

在 Spring 源码中,调用完 createInstance 方法后,然后就把当前 Bean 加入到 SingletonFactory 中,也就是在实例化完毕后,就加入到三级缓存中;

Spring通过三级缓存对Bean延迟初始化解决循环依赖。

具体如下:

  1. singletonObjects缓存:这是 Spring 容器用来缓存完全初始化好的单例 bean 实例的缓存。
  2. earlySingletonObjects缓存:这个缓存是用来保存被实例化但还未完全初始化的 bean (半成品)的引用。
  3. singletonFactories缓存:这个缓存保存的是用于创建 bean 实例的 ObjectFactory,用于支持循环依赖的延迟初始化。

Spring 通过这三级缓存的组合,来确保在循环依赖情况下,能够正常初始化 bean。当一个 bean 在初始化过程中需要依赖另一个还未初始化的 bean 时,Spring 会调用相应的 对象工厂来获取对应的 bean 半成品实例,这样就实现了循环依赖的延迟初始化。一旦 bean 初始化完成,它就会被移动到正式的单例缓存中。

3.3. 一层和两层缓存可以吗?

只使用一级缓存的情况,是不能够解决循环依赖的,有下面两个原因:

  1. 当我们仅使用一级缓存时,Bean 在初始化完成后被放入缓存中。但这依然会导致循环依赖问题。因为依赖注入发生在初始化之前,所以在依赖注入时,无法从缓存中获取到相应的 Bean,从而再次引发循环依赖。
  2. 如果我们在 Bean 实例化后立即将其放入缓存呢?这也不可行。因为我们忽略了代理对象(Spring AOP)的存在。如果创建的 Bean 是代理对象,则必须在实例化后立即创建。然而,这会带来新的问题:JDK Proxy 代理对象仅实现了目标类的接口,这会导致依赖注入时无法找到相应的属性和方法,从而导致错误。 换句话说,提前创建的代理对象缺乏原始对象的属性和方法。

只使用二级缓存,是可以解决的,但是为什么不用呢?

  1. 对于普通对象,使用二级缓存可以解决循环依赖问题。对象实例化后,放入第一级缓存。如果其他对象需要依赖注入该对象,可以直接从第一级缓存中获取。待对象初始化完成后,再写入第二级缓存。
  2. 然而,对于代理对象而言,情况就复杂了许多。如果循环依赖注入的对象是代理对象,我们就需要在对象实例化后提前创建代理对象,也就是提前创建所有代理对象。但目前的 Spring AOP 设计中,代理对象的创建是在初始化方法中的 AnnotationAwareAspectJAutoProxyCreator 后置处理器创建的。这与 Spring AOP 的代理设计原则相悖。故Spring增加了SingletonFactory,存储着 FactoryBean。