首页 > 其他分享 >spring-2-依赖注入、循环依赖、三级缓存

spring-2-依赖注入、循环依赖、三级缓存

时间:2024-06-15 19:21:25浏览次数:26  
标签:依赖 对象 spring Bean 缓存 user UserService 注入

参考:

SpringBoot———自动装配原理

Spring 依赖注入有几种?各有什么优缺点?

Spring注解@Resource和@Autowired区别

Spring是如何解决循环依赖问题?

第二次讲Spring循环依赖,时长16分钟,我保证每一秒都是精华

1.依赖注入

1.1 依赖注入的方式

方式 优点 缺点 适用场景
属性注入 简洁,减少样板代码,直接在字段上注入依赖。 不利于测试,无法使用final字段,可能导致类的职责不清晰。 适用于简单的依赖注入,测试和重构要求不高的场景。
Setter 注入 灵活,易于理解和维护,支持可选依赖注入,Setter方法可以包含验证逻辑。 需要额外的Setter方法,可能导致代码冗长,依赖不能为final字段。 适用于依赖可选且可能变化的场景,需要在注入前进行额外处理。
构造方法注入 强制依赖注入,保证依赖完整性,支持final字段,有助于单元测试。 需要额外的构造函数,可能在有多种依赖时显得冗长。 适用于依赖项多且不可或缺的场景,需要进行严格依赖管理和单元测试。

上面3个呢,就是分别把咱们的@Autowired注解用在属性上、set方法上、构造方法上。

方便演示,修改下之前的工程,添加一个实体类User,在MyConfig中注册为Bean。

image-20240613104505052

image-20240613104512823

然后我们用一个UserService测试下。

1.1.1 属性注入

属性注入很简单,就是我们最常用的那样,直接在属性上@Autowired

@Slf4j
@Service
public class UserService {

    @Autowired
    private User user;

    void showInfo() {
        log.info("user: {}", JSON.toJSONString(user));
    }

}

image-20240613111351067

1.1.2 Setter方法注入

setter注入,就是把注解用在set方法上。

image-20240613111432017

@Slf4j
@Service
public class UserService {

    private User user;

    @Autowired
    public void setUser(User user) {
        this.user = user;
    }

    void showInfo() {
        log.info("user: {}", JSON.toJSONString(user));
    }

}

1.1.3 构造方法注入

image-20240613111527619

@Slf4j
@Service
public class UserService {

    private User user;

    @Autowired
    public UserService(User user) {
        this.user = user;
    }

    void showInfo() {
        log.info("user: {}", JSON.toJSONString(user));
    }

}

1.2 依赖注入原理

现在,我们来研究下UserService中的user对象怎么来的。

还记得前面嘛,我们反射创建Bean后,会根据Bean中的属性来进行注入。

直接debug到之前提到的。

// 实例化所有剩余的(非lazy-init)单例。
finishBeanFactoryInitialization(beanFactory);

位于org/springframework/context/support/AbstractApplicationContext.java

image-20240613112425721

F7直接步入进去,看咋创建。

嗯,上一步的入参是beanFactory,这里直接调用beanFactory的方法。

image-20240613112618996

继续进去,来到了preInstantiateSingletons的方法内部,哎?this是谁,this不就是咱们beanFactory自己吗。

image-20240613112733224

右键加上条件断点,注意啊,我是加在for里面的那行,条件断点不就是让代码执行到满足条件的情况嘛。

image-20240613113121214

image-20240613113335967

然后下面的if又判断是不是FactoryBean,我们不是,所以,咱直接关掉这个if,不就到了创建了嘛。

image-20240613113601099

之后的过程就比较复杂了,会走到org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java的populateBean方法中。

image-20240613131317104

把条件断点打成

bp.getClass().getName().contains("AutowiredAnnotationBeanPostProcessor")

image-20240613130530970

这里,就是根据不同的bp(BeanPostProcessor)来执行对应的postProcessProperties方法。

比如,我们此处条件断点到了。

image-20240613131533510

而我们的org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java就是在注入Autowired的属性。

image-20240613131630647

经过这个方法后,其中的user对象已经能够被注入了。

image-20240613131715870

那么这个AutowiredAnnotationBeanPostProcessor.java中具体的注入逻辑是啥呢。

image-20240613132504265

没事,我们直接把干扰的代码去掉。

@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    Field field = (Field) this.member;
    // 查找值
    Object value = this.cached ? resolvedCachedArgument(beanName, this.cachedFieldValue) : resolveFieldValue(field, bean, beanName);
    
    // 注入值
    ReflectionUtils.makeAccessible(field);
    field.set(bean, value);
}

好,那注入的逻辑就是BeanPostProcessor的不同,比如我们换成通用注解@Resource,那这里的条件断点就修改成这样。

image-20240613133135574

1.3 @Autowired和@Resource

项目 @Resource @Autowired
出处 Java注解 (javax.annotation.Resource) Spring注解 (org.springframework.beans.factory.annotation.Autowired)
默认自动装配策略 名称优先 类型优先
名称自动装配 支持 通过@Qualifier支持
类型自动装配 名称未找到时按类型 默认
可空性 不适用 可以为null(required=false)
类级别注解 支持 不适用

2.循环依赖

2.1 是什么

循环依赖问题,讲的就是两个Bean对象互相依赖,你中有我,我中有你。

例如下方的UserService其中需要注入我们的PasswordService,而PasswordService呢又需要一个UserService。

  • 要创建UserService,你得把PasswordService先给我呀。
  • 要创建PasswordService,你得把UserService先给我呀。

???

@Slf4j
@Service
public class UserService {
    @Resource
    private PasswordService passwordService;

}
@Service
public class PasswordService {
    @Resource
    private UserService userService;

}

碰到这种场景,启动的时候会给你明显的报错信息,告诉你有循环依赖的问题。

image-20240613204723846

当然咯,以下这些场景都是类似的。

2.1.1 自己依赖自己

image-20240613204954971

2.1.2 双方互相依赖

image-20240613205003367

2.1.3 多方互相依赖

image-20240613205014916

2.2 解决方案

2.2.1 优化代码

好,那么什么场景下可能导致这个问题,以上方的UserService和PasswordService为例。

那他们既然在内部注入了,肯定就是:

  • UserService中想用PasswordService
  • PasswordService中想用UserService

最简单的方式,就是抽离出第三方类,聚合这两个玩意,让它们不要在内部互相依赖。

@Service
public class CommonService {

    @Resource
    private UserService userService;

    @Resource
    private PasswordService passwordService;

}

然后将我们涉及到依赖的逻辑抽离到这个CommonService中,用CommonService来完成操作。

2.2.2 三级缓存

image-20240615190642921

2.2.2.1 回顾Bean的创建

1.普通的Bean

首先呢,我们看下Bean是怎么获取的,从我们的BeanFactory的单例池中来拿到。

image-20240615174825330

单例池中没有,我们就会尝试创建这个Bean。

下方演示一个简单的bean对象创建,其中包含一个属性id。

  • 获取Bean:getBean(a) 获取这个bean(发现没有)

  • 实例化:没有,则反射创建这个Bean实例。

  • 填充属性:得到Bean实例后,通过populateBean()来填充其中的属性id。

  • 初始化:最后执行下我们的初始化方法,例如之前提到的postProcessAfterInitialization(在bean初始化之前执行)的一些初始化方法。

  • 放到单例池中去

image-20240615175131246

2.属性中包含别的对象的bean

那么假设对象A中还有个对象B呢?无非就是填充属性的时候,把这个B创建出来。

image-20240615175725951

3.属性产生循环依赖的bean

当B对象中需要A对象时,创建则会产生死循环。

  • a对象中需要依赖b对象,则开始b对象的创建。
  • b对象中执行属性填充的时候,尝试获取a对象。
  • 由于此时a对象也还没创建出来(单例池中没有a),那么又会触发a对象的创建。
  • 死循环

image-20240615180141572

2.2.2.2 三级缓存的引入

回顾上方的内容,我们发现问题在哪。

问题在于,填充属性的时候,getBean在单例池中没有对象可以拿,没法填充了。

所以,基本的思路就是,反射实例化后,我们先把这些对象放到半成品的池子里(没有填充对象,半成品)。

在单例池中拿不到时,尝试从半成品池子中拿。

先看b填充a属性的那里,虽然单例池拿不到a,但是我们可以从半成品池子中拿到a,进而走完Bean对象b的创建。

image-20240615180911008

拿到半成品的a后,完成b的创建,最后b被放到了单例池中去。

image-20240615181010458

最后,对象a在填充属性b的时候,单例池中已经有b能填充了,a对象也能完成创建。

创建完成a后,检查下把无意义的半成品池中的a删除掉即可。

至此,a和b都拿到了实际的对象(非半成品对象),创建完成。

image-20240615181200146

哎,看起来2级缓存已经够用了,为啥spring最后搞的是三级缓存?

因为咱们Spring中创建出来的不是普通对象,最后Spring要交付的是经过AOP代理的对象。

image-20240615181400234

即,当对象a中注入属性b的时候,注入的可不能是仅仅简单实例化的半成品b,而是要注入代理对象proxy$b

半成品池子中,存放的是未属性填充的半成品对象,不是AOP对象。

如果是aop代理,经过填充后,b里面的a是不对的。

那么这个代理对象是在啥时候创建的?在初始化阶段通过我们的postProcessAfterInitialization来创建的。

这个PostProcessor就是AspectJAwareAdvisorAutoProxyCreator

image-20240615182745957

AspectJAwareAdvisorAutoProxyCreator中有两个重要的方法。

  • postProcessBeforeInstantiation

    决定是否要为当前 Bean 创建一个代理对象。如果需要代理,就返回代理对象;否则返回 null,继续正常的 Bean 实例化过程。

  • getEarlyBeanReference

    返回一个可能的代理对象,确保在循环依赖情况下,其他 Bean 获取到的是这个 Bean 的代理对象而不是原始对象。

image-20240615184522916

image-20240615184716482

所以,现在加上AOP后,实际上应该是这个样子。

以对象a为例

  • 在初始化时,执行a的后置处理器创建代理对象proxy$A。
  • 将proxy$A放回单例池

image-20240615184843151

回到循环依赖的问题,以b填充a属性为例,关键点是我们属性填充的时候,填不出来一个动态代理的proxy$A啊。

要是可以把初始化阶段的后置处理提前就好了

实际上就是这样做的,我们把创建代理动态代理的步骤提前,打包成一个工厂方法,故而产生了我们的第3个池子,工厂池。

我们调用这个工厂池中跟这个对象绑定的工厂方法,从而提前的来创建出这个动态代理。

image-20240615185852867

所以,我们就能把半成品池子中的对象替换成最后需要的代理对象。

image-20240615190204761

经过上面的操作后,我们单例池中就能存放到实际的代理对象,解决掉我们放不进去代理对象的问题。

最后回顾下整个过程中的三个缓存池。

  • singletonObjects: 单例bean缓存(完整的Bean对象)
  • earlySingletonObjects: 早期单例bean缓存(半成品对象)
  • singletonFactories: 单例bean工厂缓存(用于创建代理对象的工厂对象)

image-20240615190904905

标签:依赖,对象,spring,Bean,缓存,user,UserService,注入
From: https://www.cnblogs.com/yang37/p/18249629

相关文章

  • Spring AOP
            AOP(AspectOrientedProgramming):面向切编程。是对某⼀类事情的集中处理,例如网站的登录验证,不使用AOP的话发布文章需要一段代码进行验证、编辑文章需要验证......而使用AOP的话只需要在某⼀处配置⼀下,需要验证的地方都可以实现了。        AOP是......
  • spring boot(学习笔记第七课)
    springboot(学习笔记第七课)配置AOP,数据库操作(jdbcTemplate)学习内容:配置AOP数据库操作(jdbcTemplate)1.配置AOP如果想要在一些既存系统上对系统进行分析,监视,但是又不想改修原有的代码,可以通过AOP(AspectOfProgram)来实现。在pom.xml中引入spring-boot-starter......
  • springboot+vue+mybatis家电系统+PPT+论文+讲解+售后
    随着信息互联网购物的飞速发展,一般企业都去创建属于自己的电商平台以及购物管理系统。本文介绍了家电销售系统的开发全过程。通过分析企业对于家电销售系统的需求,创建了一个计算机管理家电销售系统的方案。文章介绍了家电销售系统的系统分析部分,包括可行性分析等,系统设计部分主......
  • 【计算机毕业设计】基于springboot的大创管理系统【源码+lw+部署文档】
    包含论文源码的压缩包较大,请私信或者加我的绿色小软件获取免责声明:资料部分来源于合法的互联网渠道收集和整理,部分自己学习积累成果,供大家学习参考与交流。收取的费用仅用于收集和整理资料耗费时间的酬劳。本人尊重原创作者或出版方,资料版权归原作者或出版方所有,本人不对所......
  • 使用 Spring Boot 的yml配置文件读取方式
    在Java项目中读取YML(YAML)配置文件有多种方式,尤其在使用SpringFramework(包括SpringBoot)时,更是提供了丰富的支持。以下是几种常见的方式来读取YML配置文件:1.使用SpringBoot的自动配置SpringBoot提供了对YML文件的自动化支持,这也是最常用的方式。1.1使用@Value......
  • 面试题——Spring
    ★1.Spring和SpringBuffer和SpringBuilder的区别?    ①值可变性      Spring中的值是final修饰的,不可变,当试图修改这个不可变值时等于创建一个新的对象。   SpringBuffer和SpringBuilder则是可变的    ②线程安全性    Spring: ......
  • 基于Java+SpringBoot+Vue前后端分离宠物管理系统(源码+万字LW+PPT+部署教程)
    博主介绍:✌全网粉丝10W+csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌一、作品包含源码+数据库+设计文档LW+PPT+全套环境和工具资源+部署教程二、项目......
  • 深入探索Spring Boot的条件装配与条件注解
    SpringBoot的条件装配(Conditional装配)是一个强大的功能,它允许你根据特定的条件来决定哪些配置类、beans或组件应该被加载到Spring应用上下文中。这有助于创建更灵活、更模块化的SpringBoot应用程序。在SpringBoot中,条件装配主要通过@Conditional注解及其派生注解(如@Co......
  • springMVC中的注解
    目录3、@RequestMapping注解3.1、功能3.2、注解的位置3.3、value属性3.4、method属性3.5、params属性(了解)3.6、headers3.7、支持ant风格的路径3.8、SpringMVC支持路径中的占位符**3、@RequestMapping注解3.1、功能就是将用户的请求和处理请求的控制器的方法关联起来,建立映射......
  • 『手撕Vue-CLI』自动安装依赖
    开篇经过『手撕Vue-CLI』拷贝模板,实现了自动下载并复制指定模板到目标目录。然而,虽然项目已复制,但其依赖并未自动安装,可能需要用户手动操作,这并不够智能。正如前文所述,我们已经了解了业务需求和背景。那么,接下来我们将直接深入探讨核心实现细节。自动安装依赖在前文中,我们已......