Spring循环依赖与三级缓存

2023-03-30 19:26:21 528


常见的循环依赖

@Component
public class Aoo {
    @Autowired
    Boo boo;
}

@Component
public class Boo {
    @Autowired
    Aoo aoo;
}
  1. A依赖B B依赖A
  2. 首先判断三级缓存中存不存在A
  3. 判断三级缓存中存不存在B
  4. A获取到B对象, 自身完成初始化, 调用getSingleton("aoo", false)获取到二级缓存的对象, 调用addSingleton从二级缓存移除, A加入一级缓存


  • Spring怎么解决的循环依赖将对象提前暴露在三级缓存中
  • 假如移除掉二级缓存. 先说结论, 是没问题的如果A是需要代理的情况下, 上文步骤3d: ObjectFactory#getObject会返回代理A对象, 直接将其存入一级缓存. 然后A再进行初始化, A的初始化阶段不会进行再次代理. 即最终完成品的对象前后是一致的. 因为org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization这个方法利用earlyProxyReferences这个属性判断早期暴露的对象是否已经被代理过了(注意其他BeanPostProcessor可能不会有这样的逻辑, 比如下文将提到的@Async), 如果已经被代理过, 则不再进行代理. 那Spring为什么不使用两级缓存而使用三级, 可能是一二级缓存合并后, 职责会混乱. 一级缓存存储完成品Bean, 二级缓存则是半成品.如果将代理逻辑提前到对象实例化后就进行, 那么一级和三级缓存同样可以解决循环依赖问题. 那spring为什么不这么做, 可能是这样违背了Spring在结合AOP跟Bean的生命周期的设计, Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理
  • 假如移除掉三级缓存, A实例化后直接放入二级缓存B注入属性时, 无法判断A是否需要代理. 如果硬要改, 当然也是能改的.
  • 假如A不是单例则A不允许早期暴露, 则B无法从三级缓存中拿到A, 则循环依赖抛出异常
  • 假如A依赖A, 即自身依赖自身@Component public class Aoo { @Autowired Aoo aoo; } 实例化A后, 开始注入属性, 发现依赖A, 调用getBean方法获取A对象, 其中getSingleton方法将提前暴露的A对象转移到二级缓存后, 返回注入需要的A对象. A完成属性注入, 完成初始化, 再将A转移到一级缓存, 完成

但即使有三级缓存也无法解决构造器的循环依赖, 对象无法正常实例化, 没有操作的空间

相关方法源码注释

DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

// 返回已注册在给定名称下的单例对象(原始)。
// 检查已实例化的单例对象,还允许早期引用当前创建的单例对象(以解决循环引用)
// @beanName 要查找的bean名称
// @allowEarlyReference 是否应创建早期引用
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 判断一级缓存
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 判断二级缓存
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // 判断三级缓存
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 从三级缓存中拿到的工厂Bean获取到对象后, 从三级缓存中移除
                    singletonObject = singletonFactory.getObject();
                    // 加入二级缓存
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

DefaultSingletonBeanRegistry#addSingleton

// 将给定的单例对象添加到当前工厂的单例缓存中 用于立即注册单例对象。
protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 加入一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 从三级缓存移除
        this.singletonFactories.remove(beanName);
        // 从二级缓存移除
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

另外一个跟循环依赖相关的问题

@Async注解的坑https://www.cnblogs.com/zzyang/p/16469700.html

总结一句话就是@Async的代理是由AsyncAnnotationBeanPostProcessor实现的, 其他代理比如@Transactional@AspectAnnotationAwareAspectJAutoProxyCreator实现的, AnnotationAwareAspectJAutoProxyCreator调用父类AbstractAutoProxyCreator#postProcessAfterInitialization方法, 里面会判断当前bean是否是被早期暴露的, 如果是, 不进行处理. 但是AsyncAnnotationBeanPostProcessor会直接进行代理, 所以造成前后对象并不一致, 然后抛出异常

if (earlySingletonExposure) {
  // 获取到早期暴露出去的对象
  Object earlySingletonReference = getSingleton(beanName, false);
  // 早期暴露的对象不为null, 说明出现了循环依赖  
  if (earlySingletonReference != null) {
      // exposedObject是由上文initializeBean(beanName, exposedObject, mbd)返回的
      // 上文的initializeBean逻辑会调用实现了BeanPostProcessor的类对bean进行处理
      // 比如 @Async注解调用的就AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization进行处理
      // 如果被代理了initializeBean方法返回的exposedObject 就会和 初始bean 不一样
      // 这个判断的意思就是指 postProcessAfterInitialization 回调没有进行动态代理
      // 如果没有那么就将早期暴露出去的对象赋值给最终生成出去的对象并返回
      // 这样就实现了早期暴露出去的对象和最终生成的对象是同一个了
      // 但是一旦 postProcessAfterInitialization 回调生成了动态代理
      // 那么就不会走这, 也就是加了@Aysnc注解,是不会走这的
      if (exposedObject == bean) {
          exposedObject = earlySingletonReference;
      }
      else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
               // allowRawInjectionDespiteWrapping 默认是false
               String[] dependentBeans = getDependentBeans(beanName);
               Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
               for (String dependentBean : dependentBeans) {
                   if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                       actualDependentBeans.add(dependentBean);
                  }
               }
               if (!actualDependentBeans.isEmpty()) {
                   // 抛出异常 放一下翻译的异常原文
                   // 创建名为'aoo'的bean时出错:
                   // 名为'aoo'的bean以其原始版本的形式作为循环引用的一部分注入到其他bean [boo]中
                   // 但最终已被包装。这意味着其他bean不使用bean的最终版本。
                   // 这通常是过于热衷于类型匹配的结果
                   // - 例如,考虑使用“getBeanNamesForType”并关闭“allowEagerInit”标志。
                   // 也就是说你这个bean被人给用了, 你现在又改变了这个bean
                   // 所以两边不一致了, 需要抛出异常终止
                   throw new BeanCurrentlyInCreationException(beanName,
                           "Bean with name '" + beanName + "' has been injected into other beans [" +
                           StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                           "] in its raw version as part of a circular reference, but has eventually been " +
                           "wrapped. This means that said other beans do not use the final version of the " +
                           "bean. This is often the result of over-eager type matching - consider using " +
                           "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
               }
      }
   }
}

注意版本不一样实现逻辑可能有改变, 当前版本SpringBoot2.3.4

ref

https://developer.aliyun.com/article/766880

Spring RedisTemplate Scan

keys 不能用, 那就只能用scan了public static Set<String> scan(RedisTemplate<String, String> redisTemplate, String pattern) { return redisTemplate.execute((Re
2024-10-30

Shiro与SpringAOP冲突导致无法请求到Controller

各依赖版本org.crazycake:shiro-redis-spring-boot-starter:3.2.1org.springframework.boot:spring-boot-starter-aop:2.3.0.RELEASE有个配置如下@Bean public static Defaul
2023-04-16

Spring的cglib代理类无法取到被代理类的成员属性

cglib的原理是生成一个被代理类的子类进行增强, 那么为什么子类访问不到父类的属性呢@Service public class AopTestService { public String value = "hello world"; @Transactional pu
2023-04-14

Spring循环依赖与三级缓存

常见的循环依赖@Component public class Aoo { @Autowired Boo boo; } @Component public class Boo { @Autowired Aoo aoo; } A依赖B B依赖A首先判断三级缓存中存不存在
2023-03-30
观察者模式与SpringBoot应用

观察者模式与SpringBoot应用

当对象间存在一对多关系时, 则使用观察者模式(Observer Pattern). 比如, 当一个对象被修改时, 则会自动通知依赖它的对象.优点:观察者和被观察者是抽象耦合的建立一套触发机制SpringBoot应用场景在SpringBoot启动流程中org.springframework.boot.
2021-11-30

Cron 表达式 星期建议使用 MON 等单词缩写

在spring提供的定时任务框架中@Scheduled(cron = "0 0 16 ? * 1") 1指周一但, 其他cron工具中, 1指周日区别就在于, spring的一周从周一开始, 而其他工具从周日开始判断可能是spring对时区做了判断, 动态切换但暂未验证而使用 MON 表示 周一,
2021-07-19

Spring自定义注解加载和使用

Spring在扫描类信息的使用只会判断被@Component注解的类所以任何自定义的注解只要带上@Component(当然还要有String value() default "";的方法,因为Spring的Bean都是有beanName唯一标示的)都可以被Spring扫描到,并注入容器内例: Spr
2020-12-28
AOP拦截controller方法注入参数 代替@RequestBody

AOP拦截controller方法注入参数 代替@RequestBody

效果如下加上@Pass放弃aop拦截注入,使用spring mvc的参数注入核心方法package com.github.aop; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxm
2020-09-16

@Transactional可在抽象类上定义,且能生效

定义一个抽象类 import com.shopping.base.repository.sync.ipos.coupon.IposCzklbRepository; import org.springframework.beans.factory.annotation.Autowired; impor
2020-07-28

多数据源 单库操作事务不回滚

项目配置了多数据源,之前操作主数据源,直接加上@Transactional(rollbackFor = Exception.class),没有任何问题最近操作其他数据源并做测试的时候,发现int i = 1 / 0并不会回滚,各种排查,从数据库引擎到注解@Transactional使用规范和异常处理
2020-07-27
手写Spring、SpringMVC

手写Spring、SpringMVC

目前实现了以下注解@Autowired 从容器中自动注入@Bean 标记方法返回值加入容器管理@Component 标记此类加入容器管理@RestController 标记此类为Rest风格的控制器 结果默认转为json@RequestMapping 前置匹配路径@GetMapping 匹配一个Ge
2020-07-01
基于自定义注解手写权限控制

基于自定义注解手写权限控制

方法一: AOP 方法二: 拦截器项目结构项目依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-w
2020-03-28

SpringDataRedis 常用操作

//向redis里存入数据和设置缓存时间 stringRedisTemplate.opsForValue().set("test", "100",60*10,TimeUnit.SECONDS); //根据key获取缓存中的val stringRedisTemplate.opsForValue().
2020-03-28

freemarker 时间显示不正常 设置时区

项目在本地开发的时候显示正常,部署上服务器就一直差8个小时,最后发现freemarker官方文档有这样的说明time_zone:时区的名称来显示并格式化时间。 默认情况下,使用JVM的时区。 也可以是 Java 时区 API 接受的值,或者 "JVM default" (从 FreeMarker 2
2020-03-28
IDEA 2019.1 xml 不高亮

IDEA 2019.1 xml 不高亮

前几天更新了idea后,发现xml里的代码都没有了高亮,变得跟记事本一个德性了打开setting ,搜索 File Types,找到xml项, 查看下方的匹配格式,果然没有xml,(idea真是厉害)点击右方的+,输入*.xml,点击ok,解决问题
2020-03-28

npm install 淘宝镜像

npm install --registry=https://registry.npm.taobao.org
2020-03-28
Java中方法的参数传递机制

Java中方法的参数传递机制

来看一段代码 public class Man { private String name; private Integer age; public String getName() { return name; } publi
2020-03-28
基于自定义注解手写权限控制

基于自定义注解手写权限控制

方法一: AOP 方法二: 拦截器项目结构项目依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-w
2020-03-28

Docker 部署 详细全过程 附代码

Docker 部署本站 全过程环境:CentOS7.61. 安装Docker其他版本CentOS可以参考这个https://help.aliyun.com/document_detail/187598.html查看本机内核版本,内核版本需高于 3.10uname -r 确保 yum 包最新yum u
2020-03-28

SpringBoot 启动普通java工程

引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.0.9</version> </dependency>
2020-03-28

Vue.js DOM操作

<template> <input type="button" @click="reply($event)" value="回复"> </template> export default { methods: { replyFun(e) {
2020-03-29
CentOS7编译调试OpenJDK12

CentOS7编译调试OpenJDK12

1. 下载源码https://hg.openjdk.java.net/jdk/jdk12点击左侧的browse,再点击zip,就可以下载zip格式的源码压缩包。unzip xxx.zip 解压文件2. 安装jdkyum install java-11-openjdk-devel -y3. 运行con
2020-04-23
编写自己的Spring Boot Starter

编写自己的Spring Boot Starter

1.新建一个maven项目命名规则统一是xxx-spring-boot-starter完整pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"
2020-06-29