4

Spring 如何解决循环依赖

 1 year ago
source link: https://www.jansora.com/notebook/107554
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

有参考 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 级缓存,就可以满足要求。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK