1. 程式人生 > >詳解Spring 中如何控制2個bean中的初始化順序

詳解Spring 中如何控制2個bean中的初始化順序

開發過程中有這樣一個場景,2個 bean 初始化邏輯中有依賴關係,需要控制二者的初始化順序。實現方式可以有多種,本文結合目前對 Spring 的理解,嘗試列出幾種思路。

場景

假設A,B兩個 bean 都需要在初始化的時候從本地磁碟讀取檔案,其中B載入的檔案,依賴A中載入的全域性配置檔案中配置的路徑,所以需要A先於B初始化,此外A中的配置改變後也需要觸發B的重新載入邏輯,所以A,B需要注入彼此。

對於下面的模型,問題簡化為:我們需要initA()先於initB()得到執行。

@Service
public class A {
  @Autowired
  private B b;

  public A() {
    System.out.println("A construct");
  }

  @PostConstruct
  public void init() {
    initA();
  }

  private void initA() {
    System.out.println("A init");
  }
}

@Service
public class B {
  @Autowired
  private A a;

  public B() {
    System.out.println("B construct");
  }

  @PostConstruct
  public void init() {
    initB();
  }

  private void initB(){
    System.out.println("B init");
  }
}

方案一:立Flag

我們可以在業務層自己控制A,B的初始化順序,在A中設定一個“是否初始化的”標記,B初始化前檢測A是否得以初始化,如果沒有則呼叫A的初始化方法,所謂的check-and-act。對於上述模型,實現如下:

@Service
public class A {

  private static volatile boolean initialized;

  @Autowired
  private B b;

  public A() {
    System.out.println("A construct");
  }

  @PostConstruct
  public void init() {
    initA();
  }

  public boolean isInitialized() {
    return initialized;
  }

  public void initA() {
    if (!isInitialized()) {
      System.out.println("A init");
    }
    initialized = true;
  }
}

@Service
public class B {

  @Autowired
  private A a;


  public B() {
    System.out.println("B construct");
  }

  @PostConstruct
  public void init() {
    initB();
  }


  private void initB() {
    if (!a.isInitialized()) {
      a.initA();
    }
    System.out.println("B init");
  }

執行效果:

A construct
B construct
A init
B init

這種立flag的方法好處是可以做到lazy initialization,但是如果類似邏輯很多的話程式碼中到處充斥著類似程式碼,不優雅,所以考慮是否框架本身就可以滿足我們的需要。

方案二:使用DependsOn

Spring 中的 DependsOn 註解可以保證被依賴的bean先於當前bean被容器建立,但是如果不理解Spring中bean載入過程會對 DependsOn 有誤解,自己也確實踩過坑。對於上述模型,如果在B上加上註解@DependsOn({"a"}),得到的執行結果是:

A construct
B construct
B init
A init

在這裡問題的關鍵是:bean屬性的注入是在初始化方法呼叫之前。

// 程式碼位置:AbstractAutowireCapableBeanFactory.doCreateBean
// 填充 bean 的各個屬性,包括依賴注入
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
  // 呼叫初始化方法,如果是 InitializingBean 則先呼叫 afterPropertiesSet 然後呼叫自定義的init-method 方法
  exposedObject = initializeBean(beanName, exposedObject, mbd);
}

結合本例,發生的實際情況是,因為出現了迴圈依賴,A依賴B,載入B,B依賴A,所以得到了一個提前暴露的A,然後呼叫B的初始化方法,接著回到A的初始化方法。具體原始碼分析過程如下:

ApplicationContext 在 refresh 過程中的最後會載入所有的 no-lazy 單例。

本例中,先載入的bean A,最終通過無參構造器構造,然後,繼續屬性填充(populateBean),發現需要注入 bean B。所以轉而載入 bean B(遞迴呼叫 getBean())。此時發現 bean B 需要 DependsOn("a"),在儲存依賴關係(為了防止迴圈 depends)後,呼叫 getBean("a"),此時會得到提前暴露的 bean A ,所以繼續 B 的載入,流程為: 初始化策略構造例項 -> 屬性填充(同樣會注入提前暴露的 bean A ) -> 呼叫初始化方法。

// 程式碼位置:AbstractBeanFactory.doGetBean
// Guarantee initialization of beans that the current bean depends on. 例項化依賴的 bean
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
      for (String dep : dependsOn) {
        if (isDependent(beanName, dep)) {
          throw new BeanCreationException(mbd.getResourceDescription(),
              beanName, "Circular depends-on relationship between '"
              + beanName + "' and '" + dep + "'");
        }
        registerDependentBean(dep, beanName); // 快取 bean 依賴的關係
        getBean(dep);
      }
    }

得到提前暴露的 bean A的過程為:

此時此刻,bean A 的屬性注入完成了, 返回到呼叫初始化方法,所以表現的行為是:構造A -> 構造B -> B初始化 -> A初始化。

DependsOn只是保證的被依賴的bean先於當前bean被例項化,被建立,所以如果要採用這種方式實現bean初始化順序的控制,那麼可以把初始化邏輯放在建構函式中,但是複雜耗時的邏輯仿造構造器中是不合適的,會影響系統啟動速度。

方案三:容器載入bean之前

Spring 框架中很多地方都為我們提供了擴充套件點,很好的體現了開閉原則(OCP)。其中 BeanFactoryPostProcessor 可以允許我們在容器載入任何bean之前修改應用上下文中的BeanDefinition(從XML配置檔案或者配置類中解析得到的bean資訊,用於後續例項化bean)。

在本例中,就可以把A的初始化邏輯放在一個 BeanFactoryPostProcessor 中。

@Component
public class ABeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    A.initA();
  }
}

執行效果:

A init
A construct
B construct
B init

這種方式把A中的初始化邏輯放到了載入bean之前,很適合載入系統全域性配置,但是這種方式中初始化邏輯不能依賴bean的狀態。

方案四:事件監聽器的有序性

Spring 中的 Ordered 也是一個很重要的元件,很多邏輯中都會判斷物件是否實現了 Ordered 介面,如果實現了就會先進行排序操作。比如在事件釋出的時候,對獲取到的 ApplicationListener 會先進行排序。

// 程式碼位置:AbstractApplicationEventMulticaster.ListenerRetriever.getApplicationListeners()
public Collection<ApplicationListener<?>> getApplicationListeners() {
    LinkedList<ApplicationListener<?>> allListeners = new LinkedList<ApplicationListener<?>>();
    for (ApplicationListener<?> listener : this.applicationListeners) {
      allListeners.add(listener);
    }
    if (!this.applicationListenerBeans.isEmpty()) {
      BeanFactory beanFactory = getBeanFactory();
      for (String listenerBeanName : this.applicationListenerBeans) {
        try {
          ApplicationListener<?> listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
          if (this.preFiltered || !allListeners.contains(listener)) {
            allListeners.add(listener);
          }
        } catch (NoSuchBeanDefinitionException ex) {
          // Singleton listener instance (without backing bean definition) disappeared -
          // probably in the middle of the destruction phase
        }
      }
    }
    AnnotationAwareOrderComparator.sort(allListeners); // 排序
    return allListeners;
  }

所以可以利用事件監聽器在處理事件時的有序性,在應用上下文 refresh 完成後,分別實現A,B中對應的初始化邏輯。

@Component
public class ApplicationListenerA implements ApplicationListener<ApplicationContextEvent>, Ordered {
  @Override
  public void onApplicationEvent(ApplicationContextEvent event) {
    initA();
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE; // 比 ApplicationListenerB 優先順序高
  }

  public static void initA() {
    System.out.println("A init");
  }
}

@Component
public class ApplicationListenerB implements ApplicationListener<ApplicationContextEvent>, Ordered{
  @Override
  public void onApplicationEvent(ApplicationContextEvent event) {
    initB();
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE -1;
  }

  private void initB() {
    System.out.println("B init");
  }
}

執行效果:

A construct
B construct
A init
B init

這種方式就是站在事件響應的角度,上下文載入完成後,先實現A邏輯,然後實現B邏輯。