服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - Spring 处理循环依赖只使用二级缓存,可以吗?

Spring 处理循环依赖只使用二级缓存,可以吗?

2022-01-12 23:44Java识堂李立敏 Java教程

先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成。

Spring 处理循环依赖只使用二级缓存,可以吗?

什么是循环依赖?

先说一下什么是循环依赖,Spring在初始化A的时候需要注入B,而初始化B的时候需要注入A,在Spring启动后这2个Bean都要被初始化完成。

Spring的循环依赖有4种场景:

  • 构造器的循环依赖(singleton,prototype)
  • 属性的循环依赖(singleton,prototype)

「spring目前只支持singleton类型的属性循环依赖」

构造器的循环依赖

  1. @Component 
  2. public class ConstructorA { 
  3.  
  4.  private ConstructorB constructorB; 
  5.  
  6.  @Autowired 
  7.  public ConstructorA(ConstructorB constructorB) { 
  8.   this.constructorB = constructorB; 
  9.  } 
  1. @Component 
  2. public class ConstructorB { 
  3.  
  4.  private ConstructorA constructorA; 
  5.  
  6.  @Autowired 
  7.  public ConstructorB(ConstructorA constructorA) { 
  8.   this.constructorA = constructorA; 
  9.  } 
  1. @Configuration 
  2. @ComponentScan("com.javashitang.dependency.constructor"
  3. public class ConstructorConfig { 
  1. public class ConstructorMain { 
  2.  
  3.  public static void main(String[] args) { 
  4.   AnnotationConfigApplicationContext context = 
  5.     new AnnotationConfigApplicationContext(ConstructorConfig.class); 
  6.   System.out.println(context.getBean(ConstructorA.class)); 
  7.   System.out.println(context.getBean(ConstructorB.class)); 
  8.  } 

运行ConstructorMain的main方法的时候会在第一行就报异常,说明Spring没办法初始化所有的Bean,即上面这种形式的循环依赖Spring无法解决。

「构造器的循环依赖,可以在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象完成注入」

  1. @Autowired 
  2. public ConstructorB(@Lazy ConstructorA constructorA) { 
  3.  this.constructorA = constructorA; 

因为我们主要关注属性的循环依赖,构造器的循环依赖就不做过多分析了。

属性的循环依赖

先演示一下什么是属性的循环依赖。

  1. @Data 
  2. @Component 
  3. public class A { 
  4.  
  5.     @Autowired 
  6.     private B b; 
  1. @Data 
  2. @Component 
  3. public class B { 
  4.  
  5.     @Autowired 
  6.     private A a; 
  1. @Configuration 
  2. @EnableAspectJAutoProxy 
  3. @ComponentScan("com.javashitang.dependency"
  4. public class Config { 
  1. public class Main { 
  2.  
  3.     public static void main(String[] args) { 
  4.         AnnotationConfigApplicationContext context = 
  5.                 new AnnotationConfigApplicationContext(Config.class); 
  6.         System.out.println(context.getBean(A.class).getB() == context.getBean(B.class)); 
  7.         System.out.println(context.getBean(B.class).getA() == context.getBean(A.class)); 
  8.     } 

Spring容器正常启动,运行结果为true,想实现类似的功能并不难,我写个demo演示一下。

  1. public class DependencyDemoV1 { 
  2.  
  3.     private static final Map<String, Object> singletonObjects = 
  4.             new HashMap<>(256); 
  5.  
  6.     @SneakyThrows 
  7.     public static <T> T getBean(Class<T> beanClass) { 
  8.         String beanName = beanClass.getSimpleName(); 
  9.         if (singletonObjects.containsKey(beanName)) { 
  10.             return (T) singletonObjects.get(beanName); 
  11.         } 
  12.         // 实例化bean 
  13.         Object object = beanClass.getDeclaredConstructor().newInstance(); 
  14.         singletonObjects.put(beanName, object); 
  15.         // 开始初始化bean,即填充属性 
  16.         Field[] fields = object.getClass().getDeclaredFields(); 
  17.         for (Field field : fields) { 
  18.             field.setAccessible(true); 
  19.             // 获取需要注入字段的class 
  20.             Class<?> fieldClass = field.getType(); 
  21.             field.set(object, getBean(fieldClass)); 
  22.         } 
  23.         return (T) object; 
  24.     } 
  25.  
  26.     public static void main(String[] args) { 
  27.         // 假装扫描出来的类 
  28.         Class[] classes = {A.class, B.class}; 
  29.         for (Class aClass : classes) { 
  30.             getBean(aClass); 
  31.         } 
  32.         System.out.println(getBean(A.class).getB() == getBean(B.class)); 
  33.         System.out.println(getBean(B.class).getA() == getBean(A.class)); 
  34.     } 
  35.  

「在开始后面的内容的时候,我们先明确2个概念」

实例化:调用构造函数将对象创建出来 初始化:调用构造函数将对象创建出来后,给对象的属性也被赋值。

可以看到只用了一个map就实现了循环依赖的实现,但这种实现有个小缺陷,singletonObjects中的类有可能只是完成了实例化,并没有完成初始化。

而在spring中singletonObjects中的类都完成了初始化,因为我们取单例Bean的时候都是从singletonObjects中取的,不可能让我们获取到没有初始化完成的对象。

所以我们来写第二个实现,「用singletonObjects存初始化完成的对象,而用earlySingletonObjects暂存实例化完成的对象,等对象初始化完毕再将对象放入singletonObjects,并从earlySingletonObjects删除」。

  1. public class DependencyDemoV2 { 
  2.  
  3.     private static final Map<String, Object> singletonObjects = 
  4.             new HashMap<>(256); 
  5.  
  6.     private static final Map<String, Object> earlySingletonObjects = 
  7.             new HashMap<>(256); 
  8.  
  9.     @SneakyThrows 
  10.     public static <T> T getBean(Class<T> beanClass) { 
  11.         String beanName = beanClass.getSimpleName(); 
  12.         if (singletonObjects.containsKey(beanName)) { 
  13.             return (T) singletonObjects.get(beanName); 
  14.         } 
  15.         if (earlySingletonObjects.containsKey(beanName)) { 
  16.             return (T) earlySingletonObjects.get(beanName); 
  17.         } 
  18.         // 实例化bean 
  19.         Object object = beanClass.getDeclaredConstructor().newInstance(); 
  20.         earlySingletonObjects.put(beanName, object); 
  21.         // 开始初始化bean,即填充属性 
  22.         Field[] fields = object.getClass().getDeclaredFields(); 
  23.         for (Field field : fields) { 
  24.             field.setAccessible(true); 
  25.             // 获取需要注入字段的class 
  26.             Class<?> fieldClass = field.getType(); 
  27.             field.set(object, getBean(fieldClass)); 
  28.         } 
  29.         singletonObjects.put(beanName, object); 
  30.         earlySingletonObjects.remove(beanName); 
  31.         return (T) object; 
  32.     } 
  33.  
  34.     public static void main(String[] args) { 
  35.         // 假装扫描出来的类 
  36.         Class[] classes = {A.class, B.class}; 
  37.         for (Class aClass : classes) { 
  38.             getBean(aClass); 
  39.         } 
  40.         System.out.println(getBean(A.class).getB() == getBean(B.class)); 
  41.         System.out.println(getBean(B.class).getA() == getBean(A.class)); 
  42.     } 
  43.  

现在的实现和spring保持一致了,并且只用了2级缓存。spring为什么搞第三个缓存呢?「第三个缓存主要和代理对象相关」

我还是把上面的例子改进一下,改成用3级缓存的实现:

  1. public interface ObjectFactory<T> { 
  2.     T getObject(); 
  1. public class DependencyDemoV3 { 
  2.  
  3.     private static final Map<String, Object> singletonObjects = 
  4.             new HashMap<>(256); 
  5.  
  6.     private static final Map<String, Object> earlySingletonObjects = 
  7.             new HashMap<>(256); 
  8.  
  9.     private static final Map<String, ObjectFactory<?>> singletonFactories = 
  10.             new HashMap<>(256); 
  11.  
  12.     @SneakyThrows 
  13.     public static <T> T getBean(Class<T> beanClass) { 
  14.         String beanName = beanClass.getSimpleName(); 
  15.         if (singletonObjects.containsKey(beanName)) { 
  16.             return (T) singletonObjects.get(beanName); 
  17.         } 
  18.         if (earlySingletonObjects.containsKey(beanName)) { 
  19.             return (T) earlySingletonObjects.get(beanName); 
  20.         } 
  21.         ObjectFactory<?> singletonFactory = singletonFactories.get(beanName); 
  22.         if (singletonFactory != null) { 
  23.             return (T) singletonFactory.getObject(); 
  24.         } 
  25.         // 实例化bean 
  26.         Object object = beanClass.getDeclaredConstructor().newInstance(); 
  27.         singletonFactories.put(beanName, () -> { 
  28.             Object proxy = createProxy(object); 
  29.             singletonFactories.remove(beanName); 
  30.             earlySingletonObjects.put(beanName, proxy); 
  31.             return proxy; 
  32.         }); 
  33.         // 开始初始化bean,即填充属性 
  34.         Field[] fields = object.getClass().getDeclaredFields(); 
  35.         for (Field field : fields) { 
  36.             field.setAccessible(true); 
  37.             // 获取需要注入字段的class 
  38.             Class<?> fieldClass = field.getType(); 
  39.             field.set(object, getBean(fieldClass)); 
  40.         } 
  41.         createProxy(object); 
  42.         singletonObjects.put(beanName, object); 
  43.         singletonFactories.remove(beanName); 
  44.         earlySingletonObjects.remove(beanName); 
  45.         return (T) object; 
  46.     } 
  47.  
  48.     public static Object createProxy(Object object) { 
  49.         // 因为这个方法有可能被执行2次,所以这里应该有个判断 
  50.         // 如果之前提前进行过aop操作则直接返回,知道意思就行,不写了哈 
  51.         // 需要aop的话则返回代理对象,否则返回传入的对象 
  52.         return object; 
  53.     } 
  54.  
  55.     public static void main(String[] args) { 
  56.         // 假装扫描出来的类 
  57.         Class[] classes = {A.class, B.class}; 
  58.         for (Class aClass : classes) { 
  59.             getBean(aClass); 
  60.         } 
  61.         System.out.println(getBean(A.class).getB() == getBean(B.class)); 
  62.         System.out.println(getBean(B.class).getA() == getBean(A.class)); 
  63.     } 
  64.  

「为什么要包装一个ObjectFactory对象?」

如果创建的Bean有对应的aop代理,那其他对象注入时,注入的应该是对应的代理对象;「但是Spring无法提前知道这个对象是不是有循环依赖的情况」,而正常情况下(没有循环依赖情况),Spring都是在对象初始化后才创建对应的代理。这时候Spring有两个选择:

  • 不管有没有循环依赖,实例化后就直接创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入(只需要2级缓存,singletonObjects和earlySingletonObjects即可)
  • 「不提前创建好代理对象,在出现循环依赖被其他对象注入时,才提前生成代理对象(此时只完成了实例化)。这样在没有循环依赖的情况下,Bean还是在初始化完成才生成代理对象」(需要3级缓存)
  • 「所以到现在为止你知道3级缓存的作用了把,主要是为了正常情况下,代理对象能在初始化完成后生成,而不用提前生成」
缓存 说明
singletonObjects 第一级缓存,存放初始化完成的Bean
earlySingletonObjects 第二级缓存,存放实例化完成的Bean,有可能被进行了代理
singletonFactories 延迟生成代理对象

源码解析

获取Bean的时候先尝试从3级缓存中获取,和我们上面的Demo差不多哈!

DefaultSingletonBeanRegistry#getSingleton

Spring 处理循环依赖只使用二级缓存,可以吗?

当从缓存中获取不到时,会进行创建 AbstractAutowireCapableBeanFactory#doCreateBean(删除了部分代码哈)

Spring 处理循环依赖只使用二级缓存,可以吗?

发生循环依赖时,会从工厂里获取代理对象哈!

Spring 处理循环依赖只使用二级缓存,可以吗?

当开启aop代理时,SmartInstantiationAwareBeanPostProcessor的一个实现类有AbstractAutoProxyCreator

AbstractAutoProxyCreator#getEarlyBeanReference

Spring 处理循环依赖只使用二级缓存,可以吗?

getEarlyBeanReference方法提前进行代理,为了防止后面再次进行代理,需要用earlyProxyReferences记录一下,这个Bean已经被代理过了,不用再代理了。

AbstractAutoProxyCreator#postProcessAfterInitialization

Spring 处理循环依赖只使用二级缓存,可以吗?

这个方法是进行aop代理的地方,因为有可能提前代理了,所以先根据earlyProxyReferences判断一下,是否提前代理了,提前代理过就不用代理了。

当bean初始化完毕,会放入一级缓存,并从二三级缓存删除。

DefaultSingletonBeanRegistry#addSingleton

Spring 处理循环依赖只使用二级缓存,可以吗?

发生循环依赖时,整体的执行流程如下:

Spring 处理循环依赖只使用二级缓存,可以吗?

原文链接:https://mp.weixin.qq.com/s/WfY0jT2yukXgJ0pOkK6RuA

延伸 · 阅读

精彩推荐
  • Java教程20个非常实用的Java程序代码片段

    20个非常实用的Java程序代码片段

    这篇文章主要为大家分享了20个非常实用的Java程序片段,对java开发项目有所帮助,感兴趣的小伙伴们可以参考一下 ...

    lijiao5352020-04-06
  • Java教程Java实现抢红包功能

    Java实现抢红包功能

    这篇文章主要为大家详细介绍了Java实现抢红包功能,采用多线程模拟多人同时抢红包,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙...

    littleschemer13532021-05-16
  • Java教程升级IDEA后Lombok不能使用的解决方法

    升级IDEA后Lombok不能使用的解决方法

    最近看到提示IDEA提示升级,寻思已经有好久没有升过级了。升级完毕重启之后,突然发现好多错误,本文就来介绍一下如何解决,感兴趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java BufferWriter写文件写不进去或缺失数据的解决

    Java BufferWriter写文件写不进去或缺失数据的解决

    这篇文章主要介绍了Java BufferWriter写文件写不进去或缺失数据的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望...

    spcoder14552021-10-18
  • Java教程xml与Java对象的转换详解

    xml与Java对象的转换详解

    这篇文章主要介绍了xml与Java对象的转换详解的相关资料,需要的朋友可以参考下...

    Java教程网2942020-09-17
  • Java教程Java8中Stream使用的一个注意事项

    Java8中Stream使用的一个注意事项

    最近在工作中发现了对于集合操作转换的神器,java8新特性 stream,但在使用中遇到了一个非常重要的注意点,所以这篇文章主要给大家介绍了关于Java8中S...

    阿杜7482021-02-04
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    这篇文章主要介绍了Java使用SAX解析xml的示例,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程小米推送Java代码

    小米推送Java代码

    今天小编就为大家分享一篇关于小米推送Java代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧...

    富贵稳中求8032021-07-12