天天看點

spring總結(三十六)--spring解決循環依賴為什麼要用三級緩存?

原文連結

關注“蘇三說技術”,回複:開發手冊、時間管理 有驚喜。

也許有些朋友對spring的循環依賴問題并不了解,讓我們先一起看看這個例子。

@Service
public class AService {

    private BService bService;

    public AService(BService bService) {
        this.bService = bService;
    }

    public void doA() {
        System.out.println("call doA");
    }
}
           
@Service
public class BService {

    private AService aService;

    public BService(AService aService) {
        this.aService = aService;
    }

    public void doB() {
        System.out.println("call doB");
    }
}
           
@RequestMapping("/test")
@RestController
public class TestController {

    @Autowired
    private AService aService;

    @RequestMapping("/doSameThing")
    public String doSameThing() {
        aService.doA();
        return "success";
    }
}
           
@SpringBootApplication
public class Application {

    /**
     * 程式入口
     * @param args 程式輸入參數
     */
    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    }
}
           

我們在運作Application類的main方法啟動服務時,報了如下異常:

Requested bean is currently in creation: Is there an unresolvable circular reference?
           
spring總結(三十六)--spring解決循環依賴為什麼要用三級緩存?

這裡提示得很明顯,出現了循環依賴。

什麼是循環依賴?

循環依賴是執行個體a依賴于執行個體b,執行個體b又依賴于執行個體a。

或者執行個體a依賴于執行個體b,執行個體b依賴于執行個體c,執行個體c又依賴于執行個體a。

像這種多個執行個體之間的互相依賴關系構成一個環形,就是循環依賴。

為什麼會形成循環依賴?

上面的例子中AService執行個體化時會調用構造方法 public AService(BService bService),該構造方法依賴于BService的執行個體。此時BService還沒有執行個體化,需要調用構造方法public BService(AService aService)才能完成執行個體化,該構造方法巧合又需要AService的執行個體作為參數。由于AService和BService都沒有提前執行個體化,在執行個體化過程中又互相依賴對方的執行個體作為參數,這樣構成了一個死循環,是以最終都無法再執行個體化了。

spring要如何解決循環依賴?

隻需要将上面的例子稍微調整一下,不用構造函數注入,直接使用Autowired注入。

@Service
public class AService {

    @Autowired
    private BService bService;

    public AService() {
    }

    public void doA() {
        System.out.println("call doA");
    }
}
           
@Service
public class BService {

    @Autowired
    private AService aService;

    public BService() {
    }

    public void doB() {
        System.out.println("call doB");
    }
}
           

我們看到可以正常啟動了,說明循環依賴被自己解決了

spring總結(三十六)--spring解決循環依賴為什麼要用三級緩存?

spring為什麼能循環依賴?

調用applicationContext.getBean(xx)方法,最終會調到AbstractBeanFactory類的doGetBean方法。由于該方法很長,我把部分不相幹的代碼省略掉了。

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name);    Object bean;

    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
       省略........
      bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    } else {
       省略........

        if (mbd.isSingleton()) {
          sharedInstance = getSingleton(beanName, () -> {
            try {
              return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
              destroySingleton(beanName);
              throw ex;
            }
          });
          bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }

        else if (mbd.isPrototype()) {
          // It's a prototype -> create a new instance.
          Object prototypeInstance = null;
          try {
            beforePrototypeCreation(beanName);
            prototypeInstance = createBean(beanName, mbd, args);
          }
          finally {
            afterPrototypeCreation(beanName);
          }
          bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
        }
        else {
          String scopeName = mbd.getScope();
          final Scope scope = this.scopes.get(scopeName);
          if (scope == null) {
            throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
          }
          try {
            Object scopedInstance = scope.get(beanName, () -> {
              beforePrototypeCreation(beanName);
              try {
                return createBean(beanName, mbd, args);
              }
              finally {
                afterPrototypeCreation(beanName);
              }
            });
            bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
          }
          catch (IllegalStateException ex) {
            throw new BeanCreationException(beanName,
                "Scope '" + scopeName + "' is not active for the current thread; consider " +
                "defining a scoped proxy for this bean if you intend to refer to it from a singleton",
                ex);
          }
        }
      }
      catch (BeansException ex) {
        cleanupAfterBeanCreationFailure(beanName);
        throw ex;
      }
    }
    省略........
    return (T) bean;
  }
           

我們可以看到,該方法一進來會調用getSingleton方法從緩存擷取執行個體,如果擷取不到。會判斷作用域是否為:單例,多列 或者 都不是,不同的作用域建立執行個體的規則不一樣。接下來,我們重點看一下getSingleton方法。

public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
  }
           
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
          ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
          if (singletonFactory != null) {
            singletonObject = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
          }
        }
      }
    }
    return singletonObject;
  }
           

我們發現有三個Map集合:

/** Cache of singleton objects: bean name --> bean instance */
  private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

  /** Cache of singleton factories: bean name --> ObjectFactory */
  private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

  /** Cache of early singleton objects: bean name --> bean instance */
  private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
           

singletonObjects對應一級緩存,earlySingletonObjects對應二級緩存,singletonFactories對應三級緩存。

上面getSingleton方法的邏輯是:

  1. 先從singletonObjects(一級緩存)中擷取執行個體,如果可以擷取到則直接傳回singletonObject執行個體。
  2. 如果從singletonObjects(一級緩存)中擷取不對執行個體,再從earlySingletonObjects(二級緩存)中擷取執行個體,如果可以擷取到則直接傳回singletonObject執行個體。
  3. 如果從earlySingletonObjects(二級緩存)中擷取不對執行個體,則從singletonFactories(三級緩存)中擷取singletonFactory,如果擷取到則調用getObject方法建立執行個體,把建立好的執行個體放到earlySingletonObjects(二級緩存)中,并且從singletonFactories(三級緩存)删除singletonFactory執行個體,然後傳回singletonObject執行個體。
  4. 如果從singletonObjects、earlySingletonObjects和singletonFactories中都擷取不到執行個體,則singletonObject對象為空。

擷取執行個體需要調用applicationContext.getBean("xxx")方法,第一次調用getBean方法,代碼走到getSingleton方法時傳回的singletonObject對象是空的。然後接着往下執行,預設情況下bean的作用域是單例的,接下來我們重點看看這段代碼:

createBean方法會調用doCreateBean方法,該方法同樣比較長,我們把不相幹的代碼省略掉。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {

    BeanWrapper instanceWrapper = null;
    省略......
    
    if (instanceWrapper == null) {
      instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper.getWrappedInstance();
    省略........

    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
        isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    Object exposedObject = bean;
    try {
      populateBean(beanName, mbd, instanceWrapper);
      exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
      省略 .....
    }
    省略 .......
    return exposedObject;
  }
           

該方法的主要流程是:

  1. 建立bean執行個體
  2. 判斷作用域是否為單例,允許循環依賴,并且目前bean正在建立,還沒有建立完成。如果都滿足條件,則調用addSingletonFactory将bean執行個體放入緩存中。
  3. 調用populateBean方法進行依賴注入
  4. 調用initializeBean方法完成對象初始化和AOP增強

我們關注的重點可以先放到addSingletonFactory方法上。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
      if (!this.singletonObjects.containsKey(beanName)) {
        this.singletonFactories.put(beanName, singletonFactory);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
      }
    }
  }
           

該方法的邏輯是判斷如果singletonObjects(一級緩存)中找不到執行個體,則将singletonFactory執行個體放到singletonFactories(三級緩存)中,并且移除earlySingletonObjects(二級緩存)中的執行個體。

createBean方法執行完之後,會調用外層的getSingleton方法

spring總結(三十六)--spring解決循環依賴為什麼要用三級緩存?

我們重點看看這個getSingleton方法

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
      Object singletonObject = this.singletonObjects.get(beanName);
      if (singletonObject == null) {
        if (this.singletonsCurrentlyInDestruction) {
          throw new BeanCreationNotAllowedException(beanName,
              "Singleton bean creation not allowed while singletons of this factory are in destruction " +
              "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
        }
        beforeSingletonCreation(beanName);
        boolean newSingleton = false;
        boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
        if (recordSuppressedExceptions) {
          this.suppressedExceptions = new LinkedHashSet<>();
        }
        try {
          singletonObject = singletonFactory.getObject();
          newSingleton = true;
        }
        catch (IllegalStateException ex) {

          singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null) {
            throw ex;
          }
        }
        catch (BeanCreationException ex) {
          if (recordSuppressedExceptions) {
            for (Exception suppressedException : this.suppressedExceptions) {
              ex.addRelatedCause(suppressedException);
            }
          }
          throw ex;
        }
        finally {
          if (recordSuppressedExceptions) {
            this.suppressedExceptions = null;
          }
          afterSingletonCreation(beanName);
        }
        if (newSingleton) {
          addSingleton(beanName, singletonObject);
        }
      }
      return singletonObject;
    }
  }
           

該方法邏輯很簡單,就是先從singletonObjects(一級緩存)中擷取執行個體,如果擷取不到,則調用singletonFactory.getObject()方法建立一個執行個體,然後調用addSingleton方法放入singletonObjects緩存中。

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
      this.singletonObjects.put(beanName, singletonObject);
      this.singletonFactories.remove(beanName);
      this.earlySingletonObjects.remove(beanName);
      this.registeredSingletons.add(beanName);
    }
  }
           

該方法會将執行個體放入singletonObjects(一級緩存),并且删除singletonFactories(二級緩存),這樣以後再調用getBean時,都能從singletonObjects(一級緩存)中擷取到執行個體了。

說了這麼多,再回到示例中的場景。

spring總結(三十六)--spring解決循環依賴為什麼要用三級緩存?

spring為什麼要用三級緩存,而不是二級緩存?

像示例的這種情況隻用二級緩存是沒有問題的。

但是假如有這種情況:a執行個體同時依賴于b執行個體和c執行個體,b執行個體又依賴于a執行個體,c執行個體也依賴于a執行個體。

a執行個體化時,先提前暴露objectFactorya到三級緩存,調用getBean(b)依賴注入b執行個體。b執行個體化之後,提前暴露objectFactoryb到三級緩存,調用getBean(a)依賴注入a執行個體,由于提前暴露了objectFactorya,此時可以從三級緩存中擷取到a執行個體, b執行個體完成了依賴注入,更新為一級緩存。a執行個體化再getBean(c)依賴注入c執行個體,c執行個體化之後,提前暴露objectFactoryc到三級緩存,調用getBean(a)依賴注入a執行個體,由于提前暴露了objectFactorya,此時可以從二級緩存中擷取到a執行個體。注意這裡又要從二(原文說的是三,我覺得它寫錯了)級緩存中擷取a執行個體,我們知道三級緩存中的執行個體是通過調用singletonFactory.getObject()方法擷取的,傳回結果每次都可能不一樣。如果不用二(原文說的是三,我覺得它寫錯了)級緩存,這裡會有問題,兩次擷取的a執行個體不一樣。

總結:

    隻有單例的情況下才能解決循環依賴問題,并且allowCircularReferences要設定成true。

以下情況還是會出現循環依賴:

  1. 構造器注入
  2. 作用域非單例的情況,當然在自定義作用域,自己可以實作避免循環依賴的邏輯
  3. allowCircularReferences參數設定為false

最後說一句(求關注,别白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,幫忙關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

此外關注公衆号:【蘇三說技術】,在公衆号中回複:面試、代碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回複:加群,可以跟很多BAT大廠的前輩交流和學習。