Springboot为啥要有第二级缓存

创建bean的整个过程放在AbstractAutowireCapableBeanFactory的doCreateBean方法里面

java二级缓存有什么用途 spring二级缓存的作用_缓存

前面提到过Springboot的一级缓存和二级缓存一起充当深度优先遍历的vis数组,但如果只是这样的话,只需要二级缓存就足够了

三级缓存保存在DefaultSingletonBeanRegistry里面

三级缓存用来存放ObjectFactory,存放bean工厂,通过调用ObjectFactory(这个是一个接口,实现类是BeanFactory)的getObject方法创建bean,然后放入单例池中,再从二级缓存中移除

@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;
}

在递归创建其他依赖的组件前,会将未完成的组件作为放入ObjectFactory类的一个实现类里面,生成他的代理对象objectProxy,然后把这个ObjectFactory对象放入到二级缓存里面,通过调用getEarlyBeanReference来获取未完成的bean,如果这个对象没有被aop动态代理,就直接返回原来的对象,如果被aop代理过,就为这个对象生成代理对象,然后返回的就是代理对象

如果需要AOP动态代理,会将能生成当前这个对象的代理对象的ObjectFactory替换原来的ObjectFactory,通过beanPostProcessor生成代理对象

第二次调用getBean(“a”)的时候,发现一二级缓存都没有,但是第三级缓存有,就调用三级缓存的getObject方法,但是此时这个bean工厂进行被替换成了生成代理对象的bean工厂,所以可以获取到代理对象的引用(被替换为了Lamda表达式)

而b则在初始化环节,调用beanPostProccessor方法生成代理对象

所以第一个调用是使用三级缓存的getObject方法生成一个代理对象,而如果后面其他bean又引用了A,那么如果再调用三级缓存的objectFactory对象的getObject方法的话,又会生成一个新的对象,不符合单例这个条件。所以第一次调用完三级缓存的getObject方法后,就把生成的对象放入二级缓存中,这样后面再引用就从二级缓存中取得

(为啥不放入一级缓存?为了耦合度降低,职责分明)

java二级缓存有什么用途 spring二级缓存的作用_二级缓存_02

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
   Object exposedObject = bean;
   if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
         if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
            exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
         }
      }
   }
   return exposedObject;
}

普通的A依赖B,B依赖A,这个其实就是一个循环链表,是可以允许注入的,此时里面有一个allowCircularReferences参数就为true,允许循环依赖,但是如果是在@Bean注解里面注入,就要求这个bean不能为null,也不能是半成品的bean,这时候allowCircularReferences就为false

spring循环依赖 为啥需要二级缓存 debug - 知乎 (zhihu.com)

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存呢?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。所以,Spring 选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,这样就不符合Spring设计者的思想(创建对象->依赖注入->动态代理)。所以Springboot的开发者为了将创建代理对象的过程延后执行,设计了二级缓存。本来是要在依赖注入前就创建代理对象的,使用二级缓存后,Spring将创建代理对象的方法通过Lamda表达式的形式保存在一个新的ObjectFactory里面,然后替换掉原来老的ObjectFactory(而不是加入二级缓存),此时二级缓存和一级缓存里面还是没有这个对象。由于我们的getObject方法的逻辑是:

如果一级缓存没有就找二级缓存,如果二级缓存没有就找三级缓存里面的objectFactory,调用它的getObject方法创建对象。原来的objectFactory创建的是实例对象,经过提前暴露后替换为一个可以根据原来的对象,创建代理对象的objectFactory。后面的bean再调用getSingleton方法的时候,第一次会从三级缓存里面获取,此时objectFactory已经进行了替换,所以此时创建的是代理对象(如果没有aop,就返回实例对象),但是后面如果再用到不能再调用getObject方法创建新的对象,这样就不符合单例的原则,所以会把此时创建出来的对象放入二级缓存

为啥不把半成品放到单例池,单例池是存放已经完成创建的对象,把半成品放入单例池不够美观(单一职责原则)但是这个只是表面现象,下面再进行着重分析

Spring创建bean有两种方式:@Component这些注解和@Bean主键创建

上面讲述了,如果只是使用@Component和@Autowired,我们完全可以将半成品放到单例池里面,因而我们并不关心它到底是半成品还是非半成品,只要获取到它的地址,然后赋值过来就行了,所以它是在一级缓存还是在二级缓存并不重要

但是使用@Bean注解创建bean则不一样,在带有@Bean注解的方法的参数列表里面的bean,必须是单例池中的bean,而不能是半成品。试想一下,@Bean注解创建的对象是需要经过我们编写的一些语句后才能完成,这个里面我们要使用参数列表里面的各个字段和方法,如果里面有的字段还没有注入完成,我们使用这个bean就会产生一些不可预料的错误,出现一些莫名奇妙的空指针异常,这显然是不允许的(因为spring的规范里面说了,这里的bean一定都是已经完成了依赖注入的bean)。所以Spring的开发者在创建bean的时候需要检测出这个循环依赖的发生,这段代码就是doCreateBean里面的

if (earlySingletonExposure) {
   Object earlySingletonReference = getSingleton(beanName, false);
   if (earlySingletonReference != null) {
      if (exposedObject == bean) {
         exposedObject = earlySingletonReference;
      }
      else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
         String[] dependentBeans = getDependentBeans(beanName);
         Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
         for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
               actualDependentBeans.add(dependentBean);
            }
         }
         if (!actualDependentBeans.isEmpty()) {
            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 " +
                  "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
         }
      }
   }
}

假如A以@Bean的方式依赖了B,B又依赖了A

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

java二级缓存有什么用途 spring二级缓存的作用_java_03

在initialiszeBean的时候A还是半成品,所以就没法执行@Bean方法,此时,Spring就可以根据A(B所依赖的所有组件)是在一级缓存中还是在二级缓存中来判断A是不是半成品,如果是在二级缓存中,则抛出异常,说明那两个bean之间有循环依赖,如果在一级缓存里面则正常执行。而如果只有两个缓存:单例池和objectFactory,把半成品放到单例池里面,那么我们就无法区分B拿到的A到底是不是半成品,也就无法检测出循环依赖

-----------------------------分界线 2023.3.15 -------------------------------------
A依赖B,B又依赖A,这个行为在早期的Springboot中是允许的,比如2.2.6.RELEASE版本。而在比较新的版本中,比如2.6.7版本中,这个行为会被直接被视为循环依赖被检测出来。

@Component
public class A {
    @Autowired
    B b;
    public void test(){
        System.out.println("a");
    }
}
@Component
public class B {
    @Autowired
    A a;

    public void test(){
        System.out.println("b");
    }
}

2.2.6.REALEASE版本的结果

java二级缓存有什么用途 spring二级缓存的作用_java_04


2.6.7版本的结果

java二级缓存有什么用途 spring二级缓存的作用_缓存_05


所以大家可能用比较新的版本就不用思考循环依赖中如何生成代理对象的问题了,hh

小结

二级缓存的作用是为了区分成品和半成品,从而能够识别出bean之间的循环依赖