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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 依赖注入: 是指通过外部配置,将依赖关系注入到对象中。依赖注入有四种主要方式:构造器注入、setter方法注入、接口注入以及注解注入。其中注解注入在开发中最为常见,因为其使用便捷以及可维护性强;构造器注入为官方推荐,可注入不可变对象以及解决循环依赖问题。本文基于依赖注入方式引出循环依赖以及三层缓存的底层原理,以及代码的实现方式。

1. 依赖注入方式

依赖注入(DI): 是指通过外部配置,将依赖关系注入到对象中。依赖注入有四种主要方式:构造器注入、setter方法注入、接口注入以及注解注入。

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. 设计原则问题:更容易违背单一设计原则。

总结

优点

缺点

应用

构造方法注入

  1. 注入对象不可变
  2. 通用性好
  3. 避免循环依赖

代码臃肿,可读性差

依赖项是必需的,且不需要在对象生命周期内发生变化(官方推荐)

Setter 注入

  1. 需要时才被注入
  2. 符合单一职责设计原则

无法注入不可变对象

依赖项可能发生变化,或者是可选的

接口注入

  1. 规范化注入
  2. 符合单一职责设计原则
  1. 无法注入不可变对象
  2. 相对于setter注入,代码复杂

注入依赖项需要规范化或者实现快速切换时

注解注入

实现使用简单,方便维护

  1. 无法注入不可变对象
  2. 通用性差,只适用于Spring框架

高频使用

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。
目录
相关文章
|
1天前
|
Java 测试技术 开发者
Spring IoC容器通过依赖注入机制实现控制反转
【4月更文挑战第30天】Spring IoC容器通过依赖注入机制实现控制反转
22 0
|
1天前
|
缓存 Java 开发工具
【spring】如何解决循环依赖
【spring】如何解决循环依赖
13 0
|
1天前
|
XML 存储 缓存
Spring缓存是如何实现的?如何扩展使其支持过期删除功能?
总之,Spring的缓存抽象提供了一种方便的方式来实现缓存功能,并且可以与各种缓存提供商集成以支持不同的过期策略。您可以根据项目的具体需求选择适合的方式来配置和扩展Spring缓存功能。
19 0
|
1天前
|
缓存 监控 Java
【Spring系列笔记】AOP
面向切面编程就是面向特定方法编程。通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,提供一种更好的代码模块化和可维护性。 横切关注点指的是在应用程序中横跨多个模块或层的功能,例如日志记录、事务管理、安全性、缓存、异常处理等。
25 0
|
1天前
|
前端开发 Java 数据格式
【Spring系列笔记】定义Bean的方式
在Spring Boot应用程序中,定义Bean是非常常见的操作,它是构建应用程序的基础。Spring Boot提供了多种方式来定义Bean,每种方式都有其适用的场景和优势。
32 2
|
1天前
|
Java Spring 容器
【Spring系列笔记】IOC与DI
IoC 和 DI 是面向对象编程中的两个相关概念,它们主要用于解决程序中的依赖管理和解耦问题。 控制反转是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入和依赖查找。
34 2
|
1天前
|
存储 缓存 Java
【spring】06 循环依赖的分析与解决
【spring】06 循环依赖的分析与解决
9 1
|
XML Java API
Spring 依赖注入的方式,你了解哪些?
前言 依赖查找和依赖注入是 Spring 实现 IoC 容器提供的两大特性,相对于依赖查找,Spring 更推崇的是使用依赖注入,本篇先对 Spring 中依赖注入的几种方式进行介绍,后续再分享其实现。
202 0
Spring 依赖注入的方式,你了解哪些?
|
Java 测试技术 开发者
Spring 有哪几种依赖注入方式?官方是怎么建议使用的呢?
IDEA 提示 Field injection is not recommended 在使用IDEA 进行Spring 开发的时候,当你在字段上面使用@Autowired注解的时候,你会发现IDEA 会有警告提示:
256 0
Spring 有哪几种依赖注入方式?官方是怎么建议使用的呢?
http://www.vxiaotou.com