Spring 如何解决循环依赖

    有参考 https://mp.weixin.qq.com/s/djVu6SJjfYj4y_LNW-SKyw

    什么是循环依赖?

    image.png

    复现循环依赖现象

    代码

    @Component
    public class BeanA {
        @Autowired
        BeanB beanB;
    }
    
    @Component
    public class BeanB {
        @Autowired
      
      BeanA beanA;
    }
    
    @SpringBootApplication(nameGenerator = CustomBeanNameGenerator.class)
    public class CircularDependenceApplication {
        public static void main(String[] args) throws Throwable {
            SpringApplication.run(CircularDependenceApplication.class, args);
        }
    }
    

    ERROR LOG:

    Connected to the target VM, address: '127.0.0.1:58131', transport: 'socket'
    12:00:26.904 [Thread-0] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@466cb152
    
      .   ____          _            __ _ _
     /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
    ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
     \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::                (v2.7.2)
    
    2022-08-25 12:00:27.217  INFO 24885 --- [  restartedMain] c.j.d.s.CircularDependenceApplication    : Starting CircularDependenceApplication using Java 17.0.4 on JansoradeIntel-iMac.local with PID 24885 (/Users/jansora/Documents/Github/demo/backend/spring-boot/demo/target/classes started by jansora in /Users/jansora/Documents/Github/demo)
    2022-08-25 12:00:27.218 DEBUG 24885 --- [  restartedMain] c.j.d.s.CircularDependenceApplication    : Running with Spring Boot v2.7.2, Spring v5.3.22
    2022-08-25 12:00:27.218  INFO 24885 --- [  restartedMain] c.j.d.s.CircularDependenceApplication    : No active profile set, falling back to 1 default profile: "default"
    2022-08-25 12:00:27.246  INFO 24885 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
    2022-08-25 12:00:27.870  WARN 24885 --- [  restartedMain] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.jansora.demo.spring.lib.BeanA': Unsatisfied dependency expressed through field 'beanB'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.jansora.demo.spring.lib.BeanB': Unsatisfied dependency expressed through field 'beanA'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'com.jansora.demo.spring.lib.BeanA': Requested bean is currently in creation: Is there an unresolvable circular reference?
    2022-08-25 12:00:27.880  INFO 24885 --- [  restartedMain] ConditionEvaluationReportLoggingListener : 
    
    Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
    2022-08-25 12:00:27.892 ERROR 24885 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 
    
    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    The dependencies of some of the beans in the application context form a cycle:
    
    ┌─────┐
    |  com.jansora.demo.spring.lib.BeanA (field com.jansora.demo.spring.lib.BeanB com.jansora.demo.spring.lib.BeanA.beanB)
    ↑     ↓
    |  com.jansora.demo.spring.lib.BeanB (field com.jansora.demo.spring.lib.BeanA com.jansora.demo.spring.lib.BeanB.beanA)
    └─────┘
    
    
    Action:
    
    Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
    
    Disconnected from the target VM, address: '127.0.0.1:58131', transport: 'socket'
    
    Process finished with exit code 0
    

    如何解决循环依赖?

    通过三级缓存可以解决, 先看下获取单例 bean 的代码

    核心代码在 : org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

    public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    
        // ... 其他内容 ... //
        
        /** Cache of singleton objects: bean name to bean instance. */
        // 第一级缓存:用于保存实例化、注入、初始化完成的 bean 实例;
        private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
    
        /** Cache of early singleton objects: bean name to bean instance. */
        // 第二级缓存:用于保存实例化完成的 bean 实例;
        private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
        
        /** Cache of singleton factories: bean name to ObjectFactory. */
        // 第三级缓存:singletonFactories,用于保存 bean 创建工厂,以便后面有机会创建代理对象。
        private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
        
       // ... 其他内容 ... //
        
        /**
         * Return the (raw) singleton object registered under the given name.
         * <p>Checks already instantiated singletons and also allows for an early
         * reference to a currently created singleton (resolving a circular reference).
         * @param beanName the name of the bean to look for
         * @param allowEarlyReference whether early references should be created or not
         * @return the registered singleton object, or {@code null} if none found
         */
        @Nullable
        protected Object getSingleton(String beanName, boolean allowEarlyReference) {
                // Quick check for existing instance without full singleton lock
                // 从第一级缓存取 bean, 取到直接返回
                Object singletonObject = this.singletonObjects.get(beanName);
                
                // 如果 bean 在创建, 从第二级缓存取
                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 lock1
                                        // 再次确认一级缓存是否已被加载
                                        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;
        }
       // ... 其他内容 ... //
        
    }
    

    为什么要有 3 级缓存 ?

    我们先说“一级缓存”的作用,变量命名为 singletonObjects,结构是 Map<String, Object>,它就是一个单例池,将初始化好的对象放到里面,给其它线程使用,如果没有第一级缓存,程序不能保证 Spring 的单例属性。

    “二级缓存”先放放,我们直接看“三级缓存”的作用,变量命名为 singletonFactories,结构是 Map<String, ObjectFactory<?>>,Map 的 Value 是一个对象的代理工厂,所以“三级缓存”的作用,其实就是用来存放对象的代理工厂。

    那这个对象的代理工厂有什么作用呢,我先给出答案,它的主要作用是存放半成品的单例 Bean,目的是为了“打破循环”,可能大家还是不太懂,这里我再稍微解释一下。

    我们回到文章开头的例子,创建 A 对象时,会把实例化的 A 对象存入“三级缓存”,这个 A 其实是个半成品,因为没有完成 A 的依赖属性 B 的注入,所以后面当初始化 B 时,B 又要去找 A,这时就需要从“三级缓存”中拿到这个半成品的 A(这里描述,其实也不完全准确,因为不是直接拿,为了让大家好理解,我就先这样描述),打破循环。

    那我再问一个问题,为什么“三级缓存”不直接存半成品的 A,而是要存一个代理工厂呢 ?答案是因为 AOP。

    那“二级缓存”的作用就清楚了,就是用来存放对象工厂生成的对象,这个对象可能是原对象,也可能是个代理对象。

    能干掉第 2 级缓存么 ?

    假如 A 需要进行 AOP,因为代理对象每次都是生成不同的对象,如果干掉第二级缓存,只有第一、三级缓存:

    B 找到 A 时,直接通过三级缓存的工厂的代理对象,生成对象 A1。
    C 找到 A 时,直接通过三级缓存的工厂的代理对象,生成对象 A2。

    看到问题没?你通过 A 的工厂的代理对象,生成了两个不同的对象 A1 和 A2,所以为了避免这种问题的出现,我们搞个二级缓存,把 A1 存下来,下次再获取时,直接从二级缓存获取,无需再生成新的代理对象。

    所以“二级缓存”的目的是为了避免因为 AOP 创建多个对象,其中存储的是半成品的 AOP 的单例 bean。

    如果没有 AOP 的话,我们其实只要 1、3 级缓存,就可以满足要求。

    评论栏