tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 探索SpringBoot

    • 初步认识SpringBoot
    • 自定义starter
    • 一些常用注解
    • 自动装配
    • spring中开启事务
    • Spring中事务的属性
    • SpringBoot启动流程
    • Spring中Bean的创建跟循环依赖
      • Bean的创建过程
      • 循环依赖
      • 缓存结构
        • 使用缓存解决循环依赖
        • 为什么需要三级缓存、二级缓存
        • 三级缓存各层的作用
      • 依赖注入
        • 依赖注入的方式
        • 官方不推荐使用@Autowired属性注入
        • @Autowired 和 @Resource 的区别
        • 指定构造方法
        • 注解@PostConstruct
        • InitializingBean接口
      • 注意
    • IOC的实现机制是什么
  • 常用功能实现

  • Security认证授权

  • 扩展

  • 实战与注意事项

  • 其它

  • 《SpringBoot》笔记
  • 探索SpringBoot
EffectTang
2023-10-25
目录

Spring中Bean的创建跟循环依赖

# Spring中Bean的创建跟循环依赖

在上文的启动流程探索中,我们知道ioc跟bean的创建是在SpringApplication对象构造好后,执行run()方法时,再具体一点就是refreshContext()方法执行时

// 创建`IOC`容器
 context = this.createApplicationContext();
// 设置一个启动器,设置应用程序启动
context.setApplicationStartup(this.applicationStartup);
// 准备IOC容器的基本信息
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 刷新IOC容器   
this.refreshContext(context);
// 执行刷新后的处理
this.afterRefresh(context, applicationArguments);
1
2
3
4
5
6
7
8
9
10

那Bean执行过程中经历了哪些,它们之间的循环依赖又是什么?

# Bean的创建过程

一个bean的创建大概分为4个步骤:

  1. createInstance——调用构造函数,创建实例
  2. 注入依赖,property赋值
  3. 回调Aware、postProcessor..
  4. 放入缓存

# 循环依赖

那所谓的循环依赖是什么意思呢?

是指在创建Bean的第二阶段——注入依赖时,发现该Bean依赖另一个Bean,而另一个Bean的创建又依赖前一个Bean。从而导致彼此都无法创建的情况。

@Service
public class AService {

    @Autowired
    private BService bService;

}
1
2
3
4
5
6
7
@Service
public class BService {

    @Autowired
    private AService aService;
}
1
2
3
4
5
6

以上代码就是一种典型的情况,当然循环依赖也不只发生在两个Bean之间,多个Bean也可能发生,即使是单个Bean也可能发生————自己需要自己。

@Service
public class CService {

    @Autowired
    private CService cService;
}
1
2
3
4
5
6

# 缓存结构

对于循环依赖,Spring之所以能解决是因为独特的缓存结构。Spring中的缓存一共有三层:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 三级缓存
1
2
3
4
5
6

一级缓存:用来存放完全初始化好的Bean,在创建过程中的最后一步,就是放入的这一层。

二级缓存:存放的是半成品的Bean,就是尚未注入依赖的Bean,未进行一些必要的初始化

三级缓存:这里存放的则是BeanFactory(创建Bean的工厂),调用其中的方法getObject()会返回半成品的Bean,之后该半成品的Bean会被放入二级缓存中。在Bean创建的第一阶段完成后,该工厂就会被放入三级缓存中。

如果该过程中Bean的创建顺利,没有出现循环依赖,那么在Bean被创建完成后,在放入一级缓存中后,会将三级缓存中的对象工厂给删除。

# 使用缓存解决循环依赖

清楚了缓存结构,那么接下来看看它是如何解决bean创建过程中的循环依赖的。就用上面的AService跟BService例子

@Service
public class AService {

    @Autowired
    private BService bService;

}
1
2
3
4
5
6
7
  1. 调用AService的构造函数进行对象的创建,完成后会放一个ObjectFactory到三级缓存
  2. 之后进行注入依赖(给各个属性赋值),这时发现它依赖BService
  3. 接着去一级缓存中找是否有BService(之后继续在其他缓存中找),发现没有,于是就会进入BService的创建过程
  4. BService在对象创建后,同样会放一个它的ObjectFactory到三级缓存中
  5. 接着进行BService的依赖注入,发现需要AService,于是进入缓存中找,最终在三级缓存中找到了AService的ObjectFactory,于是调用其getObject方法,得到一个AService的半成品Bean,并将其放入二级缓存中,同时清除三级缓存中的A
  6. 因为AService的依赖已经找到,于是BService的Bean创建成功,因为它创建完成了,此时会将BService的bean放到一级缓存中
  7. 此时BService的bean已经有了,继续AService的bean创建,所有依赖都有了,于是最终AService的bean也会创建成功

# 为什么需要三级缓存、二级缓存

看了上面的流程,好像只用二级缓存也能解决循环依赖问题,因为只需要一个存放ObjectFactory 的缓存就可以解决循环依赖问题,或者说即使只有一级缓存也能解决循环依赖问题。为什么要设计三级缓存呢?

这是因为最终存放在一级缓存中的bean可能不是第一步中创建的原始实例,有可能是AOP中的代理对象,比如以下这种情况

@Service
public class AService{
  
  @Transactional
  public void test(){
    
  }
}
1
2
3
4
5
6
7
8

因为加了@Transactional所以在对象工厂中生成的就是代理对象,而非第一步中生成的原始对象。所以需要三级缓存,这也是三级缓存存在的主要原因。

那如果把ObjectFactory放入二级缓存呢?放到三级。这是为了延迟加载,因为不是每个bean的生成会导致循环依赖,Spring它考虑的是只有在循环依赖发生的时候,才缓存半成品bean。

同时也是为了提高效率,比如以下这种情况

@Service
public class AService{
  
  @Autowired
  private BService bService;
}

@Service
public class BService{
  
  @Autowired
  private AService aService;
  @Autowired
  private CService cService;
  
}

@Service
public class CService{
  
  @AutowireA
  private AService aService;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • 先创建A发现需要B
  • 接着创建B,发现需要A,于是创建A对象工厂并放入三级,并生成半成品A放入二级
  • 继续注入依赖C,发现没有C
  • 在创建C的过程中,发现它也依赖A,这个时候之前存的二级缓存就起到了作用,可以直接创建

总结一下:

  • 一级缓存则是存放的是实例化完成的bean
  • 二级缓存中的bean有可能是半成品对象,也有可能是代理半成品对象。
  • 三级缓存存的是ObjectFactory,主要是为了AOP的实现

# 三级缓存各层的作用

  • 一级缓存:存储完整的Bean
  • 二级缓存:避免多重循环依赖的情况 重复创建动态代理。
  • 三级缓存:
    • 缓存是函数接口:通过lambda 把方法传进去(把Beant实例和Bean名字传进去(aop创建))
    • 不会立即调
    • 会在 ABA(第二次getBean(A)才会去调用三级缓存(如果实现了aop才会创建动态代理,如果没有实现依然返回的Bean的实例))
    • 放入二级缓存(避免重复创建)

# 依赖注入

依赖注入就是将该类所需的属性(其他 Bean 或配置值)初始化。

# 依赖注入的方式

Spring 支持多种依赖注入的方式,包括:

  1. 构造函数注入
  2. setter 方法注入
  3. 字段注入

我们在日常开发中常用的是字段注入,也就是直接在属性字段上加对应的注解;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 其他方法
}
1
2
3
4
5
6
7
8
9
10
11

# 官方不推荐使用@Autowired属性注入

官方推荐使用构造器方式进行注入,这样可提升项目启动性能。属性注入可能需要在对象创建后进行多次注入操作,性能稍逊。

同时构造器的方式还可以使用final关键字,来保证属性值的不变,提高安全性。

而若是对属性值加上final修饰符,则无法通过属性进行注入。

原因如下:

属性注入(Field Injection)在 Spring 框架中是通过反射(Reflection)实现的。Spring 容器使用反射机制来访问和设置类的私有字段,从而完成依赖注入。

但加了 final 的属性字段必须在对象创建时被初始化。否则编译器会报错,提示 该属性 字段没有被初始化。即使你通过某种方式使代码编译通过,final 字段的不可变性仍然存在。Spring 框架在进行属性注入时,会尝试使用反射来设置字段的值。然而,由于 final 字段的特殊性,反射并不能可靠地更改其值。

所以,既要实现属性注入又要实现属性不可变性,是不可能的。因此,官方推荐构造器的方式注入。

不过,如今的开发中,使用属性注入的方式仍然很多,其原因在于它的便捷性和对代码量的简化。

而关于其他的问题,比如属性值的安全性等,应该是可以通过开发过程中来避免的。所以关于建议,还是要根据情况来看。

当然也有人认为,之所以这样建议是设计者认为依赖应该在初始化阶段就定义了而不是之后再定义,且任何依赖都不应为null。如果真的想传入null也应该传入null object pattern的实现而不是真的null值。

# @Autowired 和 @Resource 的区别

@Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。它们都提供了将依赖对象注入到当前bean的功能

  • 来源不同:@Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解
  • 查找顺序不同:
    • @Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找
    • @Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找
  • 支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数
  • 依赖注入的支持不同
    • @Autowired 既支持构造方法注入,又支持属性注入和 Setter注入
    • @Resource 只支持属性注入和 Setter注入

# 指定构造方法

bean的第一步是对象的构造,spring中bean的默认创建是调用无参构造方法。

当然你可以指定构造方法。大家可以用一下简单代码进行实验:

@Service
public class AService {

    private UserService userService;

    public AService() {
        System.out.println("bean Aservice 无参构造");
    }
    @Autowired
    public AService(UserService userService) {
        System.out.println("执行有参构造");
        this.userService = userService;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以上代码就是通过@Autowired来指定使用有参构造来创建对象。项目启动时,在控制台便会打印有参构造对应的语句,而不会执行无参构造。

# 注解@PostConstruct

完成依赖注入后,就进入了第三阶段,执行各种回调。在依赖注入完成后,你如果想对bean做一些操作,比如缓存一些值,或者其他。你可以通过@PostConstruct注解来实现:

@Service
public class AService {

    private UserService userService;

    public AService() {
        System.out.println("bean Aservice 无参构造");
    }

    @PostConstruct
    public void Test(){
        System.out.println("PostConstruct");
    }

    @Autowired
    public AService(UserService userService) {
        System.out.println("执行有参构造");
        this.userService = userService;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用该注解的方法会在依赖注入完成后被自动调用。它的作用是:对当前bean进行一些初始化操作。调用顺序如下:

  • Constructor >> @Autowired >> @PostConstruct

# InitializingBean接口

除了通过注解的方式可以实现:自定义方法,在依赖注入完成后自动调用外,还可以用实现接口的方式。在bean类中实现接口InitializingBean,并重写afterPropertiesSet()方法:

@Service
public class AService  implements InitializingBean {

    private UserService userService;

    public AService() {
        System.out.println("bean Aservice 无参构造");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("initializing ---");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

此时在AService完成依赖注入后,便会执行afterPropertiesSet方法

# 注意

Spring Boot 2.6.0之后,如果程序中存在循环依赖问题,启动上就会失败,报错

如果要启动,需要进行一些设置

spring.main.allow-circular-references = true
1
上次更新: 2025/04/23, 16:23:16
SpringBoot启动流程
IOC的实现机制是什么

← SpringBoot启动流程 IOC的实现机制是什么→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式