Spring采用三级缓存解决循环依赖问题,当你尝试写一个简单的ioc容器时发现根本不需要三级缓存。那Spring为什么采用三级缓存呢?
文章目录
- 什么是循环依赖?
- 手写一个简单的ioc容器
- Bean的创建流程
- Spring如何解决循环依赖?
- 三级缓存
- Spring为什么不两阶段初始化?
- Spring能解决哪些循环依赖问题?
- 提前AOP时对象还没初始化会出问题吗?
- 怎么避免重复AOP?
- 只有一级缓存可以吗?
- 只有二级缓存可以吗?
- 三级缓存为什么使用工厂?
什么是循环依赖?
在Spring中因为有ioc
和di
的特性,假如有以下代码:
@Component
class A {
@Autowired
B b;
}
@Component
class B {
@Autowired
A a;
}
两个组件,你依赖我,我依赖你。
假如先创建A对象,然后进行属性赋值,发现依赖B,转头去发现B没创建,但是它是用户需要创建的,因为标记了Component注解,所以我又去创建B,发现依赖A,此时A还没创建完呢,然后创建A,发现需要B,创建B,发现需要A…
一般来说,如果我们的代码中出现了循环依赖,则说明我们的代码在设计的过程中可能存在问题,我们应该尽量避免循环依赖的发生。不过一旦发生了循环依赖,Spring 默认也帮我们处理好了,当然这并不能说明循环依赖这种代码就没问题。
实际上在目前最新版的 Spring 中,循环依赖是要额外开启的,如果不额外配置,发生了循环依赖就直接报错了。
手写一个简单的ioc容器
下面代码,用来模拟循环依赖。
通过开关控制是否提前放入一级缓存,如果允许则可解决循环依赖,如果不允许就会递归爆栈。
package com.zhouql.spring;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ApplicationContext {
Map<String, Class<?>> beanDefinitionMap = new HashMap<>();
Map<String, Object> singletonMap = new HashMap<>();
boolean putInAdvance;
public ApplicationContext(boolean putInAdvance) {
// 是否提前放入singletonMap
this.putInAdvance = putInAdvance;
// 模拟扫描到的bean组件
beanDefinitionMap.put("a", A.class);
beanDefinitionMap.put("b", B.class);
// 对扫描到的组件初始化
beanDefinitionMap.forEach(this::createBean);
}
// 创建后需要注入
public void createBean(String name, Class<?> type) {
if (singletonMap.containsKey(name)) return;
Object bean = this.getBean(name);
if (putInAdvance) this.singletonMap.put(name, bean);
// 属性注入
List<Field> autowiredFields = getAutowiredFields(type);
if (!autowiredFields.isEmpty()) {
autowiredFields.forEach(field -> {
this.createBean(field.getName(), field.getType());
field.setAccessible(true);
try {
field.set(bean, singletonMap.get(field.getName()));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
if (!putInAdvance) this.singletonMap.put(name, bean);
}
public List<Field> getAutowiredFields(Class<?> type) {
return Arrays.stream(type.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Autowired.class))
.toList();
}
// 用户通过此API获取bean
public Object getBean(String name) {
if (!singletonMap.containsKey(name)) {
boolean requireCreate = beanDefinitionMap.containsKey(name);
if (!requireCreate) throw new RuntimeException("bean name is not support.");
try {
return beanDefinitionMap.get(name).getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return singletonMap.get(name);
}
public static void main(String[] args) {
// 通过开关控制是否提前放入一级缓存,如果允许则可解决循环依赖,如果不允许就会递归爆栈
ApplicationContext ioc = new ApplicationContext(true);
A bean = (A) ioc.getBean("a");
System.out.println(bean.b);
}
}
class A {
@Autowired
B b;
}
class B {
@Autowired
A a;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Autowired {
}
上面代码是想说,一级缓存就可以避免这个问题了。
因为对象实例化就可以被观察了,依赖注入发生在实例化以后,流程大概如下。
A实例化(放入map)
A需要B
B实例化
B需要A(从map中可以拿到,B完成)
回到A需要B(从map中拿到B,A完成)
Bean的创建流程
创建实例(createInstance)->填充属性(populateBean)->初始化(initializeBean)
& 创建实例
- 这一阶段的目标是创建 Bean 对象的实例(即调用构造函数)。
- Spring 使用反射来完成实例化。
- 已经初始化好的 Bean :是指已经完成创建 Bean 需要经历的三个阶段。
- 未初始化好的 Bean :指只经过了 Bean 创建的第一个阶段,只将 bean 实例创建出来了。
& 填充属性
- 在实例化之后,Spring 会将 Bean 所依赖的其他 Bean 注入进来,即进行依赖注入。
- 处理 @Autowired、@Value、XML 或 Java 配置中定义的依赖。
- 将其它 Bean 注入到字段、setter 或构造器中。
- 类型转换(Type Converter)也会在此阶段发生,例如字符串转枚举、字符串转 Date 等。
- 循环依赖处理:对于单例的情况,此时可以提前暴露引用,解决循环依赖。
& 初始化
- 属性填充完毕后,会调用初始化逻辑。
- 调用 Aware 接口方法(如果实现了):BeanNameAwareBeanClassLoaderAwareBeanFactoryAwareApplicationContextAware 等。
- 执行 BeanPostProcessor 前置处理(PostProcessBeforeInitialization)。
- 执行初始化方法,如果实现了 InitializingBean 接口,会调用 afterPropertiesSet() 方法。如果在配置中指定了 init-method,也会调用这个方法。如果使用了 @PostConstruct,也会在这里执行。
- 执行 BeanPostProcessor 后置处理(PostProcessAfterInitialization),比如 AOP 动态代理就是在这里生成的代理对象。
Spring如何解决循环依赖?
Spring容器的循环依赖包括构造器循环依赖和setter循环依赖。
构造器循环依赖
是不好解决的,会抛出 BeanCurrentlylnCreationException 异常。
理解这点很容易,比如A对象构造时依赖B,创建B,此时A还没创建完成,发现B依赖A,拿A,发现A没有,创建A,依次循环没有出口。
setter循环依赖
在创建单例 Bean 时,先实例化原始Bean,然后会把该 Bean 的工厂函数的匿名类对象放入三级缓存中的 singletonFactories 中。
然后在填充属性时,如果出现循环依赖依赖本 Bean,必然执行之前放入的工厂函数的匿名实现,如果该 Bean 无需 AOP 的话,工厂函数返回的就是原 Bean 对象;如果该 Bean 有 AOP 的话,也有可能是被某些 BBP 处理 AOP 之后的代理对象,会放入二级缓存中的 earlySingletonObjects 中。
接着 Bean 开始初始化,如果该 Bean 无需 AOP 的话,结果返回的原来创建的 Bean 对象;如果该 Bean 有 AOP 的话,检查 AOP 织入逻辑是否已经在提前曝光时已经执行了,如果已经执行 AOP 则返回提前曝光的代理 Bean 对象;如果 AOP 织入逻辑未执行过,则进行后续的 BeanPostProcessor 后置处理器进行 AOP 织入,生成 AOP 代理 Bean 对象,并返回。
最后对于提前曝光的单例,就会去检查初始化后的 Bean 对象与二级缓存中提前曝光的 Bean 是不是同一个对象,只有不是的情况下才可能抛出异常。
项目中出现 Bean 的循环依赖,本质原因是代码架构设计不合理,某些 facade 类实现了本应在 serivce 层的业务逻辑,导致其他业务依赖地方反复应用 facade 层对象。SpringBoot 2.6.x 以上的版本官方已经不推荐使用循环依赖,说不定今后某个最新版本的 Spring 会强制不能出现 Bean 循环依赖,因此需要我们开发者在平时编码时要重视代码架构设计。
三级缓存
- 一级缓存 singletonObjects:主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例 Bean 实例,这样的 Bean 能够直接提供给用户使用,我们称之为终态 Bean 或叫成熟 Bean。
- 二级缓存 earlySingletonObjects:创建bean过程中用于处理循环依赖的临时缓存,搭配第三层缓存,用于其ObjectFactory返回对象的缓存,保证多个关联对象对当前bean的引用为同一个
- 三级缓存 singletonFactories:创建bean过程中用于处理循环依赖的临时缓存,由于只有在初始化时才知道有没有循环依赖,所以通过ObjectFactory临时“存储”刚创建完的bean,并延迟触发循环依赖时被引用存放的是 ObjectFactory 的匿名内部类实例,调用 ObjectFactory.getObject() 最终会调用 getEarlyBeanReference 方法,该方法可以获取提前暴露的单例 Bean 引用。
Spring为什么不两阶段初始化?
什么是两阶段初始化?我们可能觉得这样不就避免了循环依赖了吗?
- 第一遍:遍历所有 BeanDefinition,new 出实例但不做任何依赖注入,也不执行初始化逻辑,把这些对象放入容器(不可用状态)。
- 第二遍:统一给每个 Bean 执行依赖注入和初始化逻辑,从“仓库”里 get 依赖即可。
Spring 为什么不这么做?这个问题的关键点在于:
一、Spring 的 Bean 初始化逻辑是用户可扩展的
比如你自己实现了:InitializingBean@PostConstruct自定义的 BeanPostProcessor、InstantiationAwareBeanPostProcessor 等。这些钩子可以在 Bean 还没完全创建好时就操作其他 Bean。比如你可能在 afterPropertiesSet() 里就调用了另一个 Mapper 查询数据库,而那个 Mapper 的 Bean 还没被初始化呢。
如果 Spring 用“两阶段”方式,到你真正调用别的 Bean 的时候,它可能还是“半成品”状态。
二、依赖注入不是简单的 set/get,而是需要构造时就准备好
特别是你用了构造器注入(@Autowired 构造方法),那你根本连 Bean 对象都没办法创建成功,因为构造参数里的依赖都还没准备好,连对象都 new 不出来,更别说放进“仓库”了。
三、循环依赖只能解决“字段注入”
Spring 本身也不能解决所有的循环依赖——只能解决“单例 + 字段注入”的情况。构造器注入 + 循环依赖,依旧会抛异常。
所以 Spring 的做法是:
- 在第一遍创建 Bean 时,如果遇到循环依赖,就在“三级缓存”里提前暴露早期 Bean(early reference),只注入必要的引用,保证可以互相注入完成。
- 最后再做完整初始化。
Spring能解决哪些循环依赖问题?
Spring 只能解决“单例 + 非构造器注入”的循环依赖。
提前AOP时对象还没初始化会出问题吗?
不会,这是因为不管是cglib代理还是jdk动态代理生成的代理类,内部都持有一个目标类的引用,当调用代理对象的方法时,实际会去调用目标对象的方法,后续目标初始化相当于代理对象自身也完成了初始化。
怎么避免重复AOP?
Spring 用一个名为 earlyProxyReferences
的 Map,记录 提前被代理过的 Bean,在常规初始化时检查这个 Map,避免重复创建代理对象。
Spring 初始化 Bean 时,会两次调用 BeanPostProcessor:
第一次:是在解决循环依赖时,调用的是 getEarlyBeanReference()
- 来自 SmartInstantiationAwareBeanPostProcessor(AOP 的 AbstractAutoProxyCreator 实现了这个接口)
- 内部也会调用 wrapIfNecessary() 尝试创建代理
- 同时将 bean 放入 earlyProxyReferences 中 ✅
第二次:是 bean 正常初始化完成后,Spring 统一调用所有 BeanPostProcessor 的 postProcessAfterInitialization()
- AOP 的逻辑也在这里再次执行,但会判断如果已经在 earlyProxyReferences 中,就不重复代理
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 👇 就是这里!
this.earlyProxyReferences.put(cacheKey, bean);
// 再次尝试包装成代理对象(如果需要)
return wrapIfNecessary(bean, beanName, cacheKey);
}
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
// 👇 核心判断(如果之前代理过,这里相等,不再代理,如果没有,这里是null尝试代理)
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 创建代理
return this.wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
/**
* 判断当前 bean 是否需要被代理(应用 AOP),如果需要,则创建代理对象
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// ✅ 1. 如果当前 beanName 是通过 TargetSource 特殊处理的(比如通过 lookup-method 或 method-injection 方式生成的)
// 那么不在这里创建代理,直接返回原始 bean
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// ✅ 2. 如果之前已经判断过这个 bean 不需要被代理,就直接返回原始 bean(缓存命中)
else if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// ✅ 3. 判断是否是基础设施类(比如 Advisor、Advice、AopInfrastructureBean 之类的),或者被 shouldSkip 筛选排除
// 如果不是这些,就尝试生成代理
else if (!this.isInfrastructureClass(bean.getClass()) && !this.shouldSkip(bean.getClass(), beanName)) {
// ✅ 4. 获取与当前 bean 匹配的拦截器(Advisor/Advice)
// 如果返回的是 DO_NOT_PROXY,说明不需要代理
Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, (TargetSource) null);
// ✅ 5. 如果返回的是可用的切面/通知(非 DO_NOT_PROXY),说明需要创建代理
if (specificInterceptors != DO_NOT_PROXY) {
// 标记当前 bean 已经被代理过
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建 AOP 代理对象
Object proxy = this.createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
// 记录代理类的类型,用于后续类型匹配等逻辑
this.proxyTypes.put(cacheKey, proxy.getClass());
// 返回代理对象
return proxy;
} else {
// 如果不需要代理,缓存结果避免下次重复判断
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}
// ✅ 6. 如果是基础设施类 或 shouldSkip 返回 true,记录缓存并返回原始 bean
else {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}
只有一级缓存可以吗?
实例化以后直接放入一级缓存,可解决部分循环引用。
但是实例化后没经历属性填充,初始化等生命周期,该bean是一个不完成的bean,可能导致业务代码执行不一致,因此需要保证一级缓存中放的bean是经历了完成的生命周期的可用对象。
只有二级缓存可以吗?
Map<String, Object> cache1; // 用于存储成熟的bean
Map<String, Object> cache2; // 用于存储实例化后的对象,提前暴露
可以的,需要调整获取bean的逻辑,先读一级缓存,没有,再读二级缓存,同时实例化后将bean放入二级缓存,初始化完成后放入一级缓存,移除二级缓存对应key。
那关于aop问题呢?其实需要在实例化时判断是否需要aop,如果需要,创建aop代理对象,如果不需要创建普通对象即可。
但是在这里直接使用二级缓存的话,那么意味着所有的Bean在实例化后,属性填充、初始化前要完成AOP代理。
违背了Spring在结合AOP跟Bean的生命周期的设计
!Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。
如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。
三级缓存为什么使用工厂?
三级缓存存储的是BeanFactory接口,一个函数式接口。
使用工厂的目的在于延迟对实例化阶段生成的对象的代理。
只有真正发生循环依赖的时候,才去提前生成代理对象(提前aop)。
否则该三级缓存直接返回原始对象了。
即使没有循环依赖,也会将其添加到三级缓存中,而且是不得不添加到三级缓存中(解决循环依赖的关键,这也就是为什么说使用三级缓存,不是指三个map,而是第三级最重要),因为到目前为止Spring也不能确定这个Bean有没有跟别的Bean出现循环依赖。