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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 依赖注入: 是指通过外部配置,将依赖关系注入到对象中。依赖注入有四种主要方式:构造器注入、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。
目录
相关文章
|
7天前
|
存储 缓存 Java
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
Spring缓存注解【@Cacheable、@CachePut、@CacheEvict、@Caching、@CacheConfig】使用及注意事项
41 2
|
2月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
170 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
2月前
|
缓存 NoSQL Java
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
缓存与分布式锁、Redisson分布式锁、缓存数据一致性【必须满足最终一致性】
112 14
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
|
1月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
25 1
|
1月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
20 1
|
2月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
38 4
|
2月前
|
存储 缓存 Java
在Spring Boot中使用缓存的技术解析
通过利用Spring Boot中的缓存支持,开发者可以轻松地实现高效和可扩展的缓存策略,进而提升应用的性能和用户体验。Spring Boot的声明式缓存抽象和对多种缓存技术的支持,使得集成和使用缓存变得前所未有的简单。无论是在开发新应用还是优化现有应用,合理地使用缓存都是提高性能的有效手段。
36 1
|
1月前
|
XML Java 数据库连接
【2020Spring编程实战笔记】Spring开发分享~(上)
【2020Spring编程实战笔记】Spring开发分享~
48 0
|
2月前
|
缓存 NoSQL Java
瑞吉外卖项目笔记+踩坑2——缓存、读写分离优化
缓存菜品、套餐数据、mysql主从复制实现读写分离、前后端分离
瑞吉外卖项目笔记+踩坑2——缓存、读写分离优化
消息中间件 缓存 监控
106 0