一步一步手繪Spring IOC執行時序圖一(Spring 核心容器 IOC初始化過程)

Jarvis.y發表於2020-10-05

前言

1、部落格內容均出自於咕泡學院架構師第三期
2、本章內容共分為三部分
第一部分:一步一步手繪Spring IOC執行時序圖一(Spring 核心容器 IOC初始化過程)
第二部分:一步一步手繪Spring IOC執行時序圖二(基於XML的IOC容器初始化)
第三部分:一步一步手繪Spring IOC執行時序圖三(基於Annotation的IOC容器初始化)

1、Spring 核心之 IOC 容器初體驗

IOC 與 DI

IOC(Inversion of Control )控制反轉:所謂控制反轉,就是把原先我們程式碼裡面需要實現的物件建立,依賴的程式碼,反轉給容器來幫忙實現。 那麼必然的我們需要建立一個容器 ,同時需要一種描述來讓容器知道需要建立的物件與物件的關係。這個描述最具體表現就是我們所看到的配置檔案。
DI(Dependency Injection)依賴注入: 就是指物件是被動接受依賴類而不是自己主動去找,換句話說就是指物件不是從容器中查詢它依賴的類而是在容器例項化物件的時候主動將它依賴的類注入給它。

先從我們自己設計這樣一個視角來考慮:
1、物件和物件的關係怎麼表示?
可以用 xml,properties 檔案等語義化配置檔案表示。

2、描述物件關係的檔案存放在哪裡?
可能是 classpath,filesystem,或者是 URL 網路資源,servletContext 等。

3、不同的配置檔案對物件的描述不一樣,如標準的,自定義宣告式的,如何統一?
在內部需要有一個統一的關於物件的定義,所有外部的描述都必須轉化成統一的描述定義。

4、如何對不同的配置檔案進行解析?
需要對不同的配置檔案語法,採用不同的解析器。

2、 Spring 核心容器類圖

BeanFactory

Spring Bean 的建立是典型的工廠模式,這一系列的 Bean 工廠,也即 IOC 容器為開發者管理物件間的依賴關係提供了很多便利和基礎服務,在 Spring 中有許多的 IOC 容器的實現供使用者選擇和使用,其相互關係如下:
在這裡插入圖片描述
其中 BeanFactory 作為最頂層的一個介面類,它定義了 IOC 容器的基本功能規範,BeanFactory 有三個重要的子類:ListableBeanFactory、HierarchicalBeanFactory 和 AutowireCapableBeanFactory。

但是從類圖中我們可以發現最終的預設實現類是 DefaultListableBeanFactory,它實現了所有的介面。 那為何要定義這麼多層次的介面呢?查閱這些介面的原始碼和說明發現,每個介面都有它使用的場合,它主要是為了區分在 Spring 內部在操作過程中物件的傳遞和轉化過程時,對物件的資料訪問所做的限制。例如 ListableBeanFactory 介面表示這些 Bean 是可列表化的,而 HierarchicalBeanFactory 表示的是這些 Bean 是有繼承關係的,也就是每個 Bean 有可能有父 Bean。AutowireCapableBeanFactory 介面定義 Bean 的自動裝配規則。這三個介面共同定義了 Bean 的集合、Bean 之間的關係、以及 Bean 行為。最基本的 IOC 容器介面 BeanFactory,來看一下它的原始碼:

public interface BeanFactory {

   //對FactoryBean的轉義定義,因為如果使用bean的名字檢索FactoryBean得到的物件是工廠生成的物件,
   //如果需要得到工廠本身,需要轉義
   String FACTORY_BEAN_PREFIX = "&";
   //根據bean的名字,獲取在IOC容器中得到bean例項
   Object getBean(String name) throws BeansException;
   //根據bean的名字和Class型別來得到bean例項,增加了型別安全驗證機制。
   <T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;
   Object getBean(String name, Object... args) throws BeansException;
   <T> T getBean(Class<T> requiredType) throws BeansException;
   <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
   //提供對bean的檢索,看看是否在IOC容器有這個名字的bean
   boolean containsBean(String name);
   //根據bean名字得到bean例項,並同時判斷這個bean是不是單例
   boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
   boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
   boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
   boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

   //得到bean例項的Class型別
   @Nullable
   Class<?> getType(String name) throws NoSuchBeanDefinitionException;

   //得到bean的別名,如果根據別名檢索,那麼其原名也會被檢索出來
   String[] getAliases(String name);

}

在BeanFactory 裡只對 IOC 容器的基本行為作了定義,根本不關心你的 Bean 是如何定義怎樣載入的。
正如我們只關心工廠裡得到什麼的產品物件,至於工廠是怎麼生產這些物件的,這個基本的介面不關心。
而要知道工廠是如何產生物件的,我們需要看具體的IOC 容器實現,Spring提供了許多IOC容器的實現 。 比如GenericApplicationContext , ClasspathXmlApplicationContext 等 。
ApplicationContext 是Spring提供的一個高階的IOC容器,也是繼承了BeanFactory,它除了能夠提供 IOC 容器的基本功能外,還為使用者提供了以下的附加服務。從 ApplicationContext 介面的實現,我們看出其特點:
1、支援資訊源,可以實現國際化。(實現 MessageSource 介面)
2、訪問資源。(實現 ResourcePatternResolver 介面)
3、支援應用事件。(實現 ApplicationEventPublisher 介面)

BeanDefinition

SpringIOC 容器管理了我們定義的各種 Bean 物件及其相互的關係,Bean 物件在 Spring 實現中是以 BeanDefinition 來描述的。 BeanDefinition 主要是用來描述 Bean,其儲存了 Bean 的相關資訊,Spring 例項化 Bean 時需讀取該 Bean 對應的 BeanDefinition。BeanDefinition 整體可以分為兩類,一類是描述通用的 Bean,還有一類是描述註解形式的 Bean。一般前者在 XML 時期定義 <bean‘> 標籤以及在 Spring 內部使用較多,而現今我們大都使用後者,通過註解形式載入 Bean。
其繼承體系如下:
在這裡插入圖片描述

BeanDefinitionReader

Bean 的解析過程非常複雜,功能被分的很細,因為這裡需要被擴充套件的地方很多,必須保證有足夠的靈活性,以應對可能的變化。Bean 的解析主要就是對 Spring 配置檔案的解析。這個解析過程主要通過BeanDefintionReader 來完成,最後看看 Spring 中 BeanDefintionReader 的類結構圖:
在這裡插入圖片描述
通過 BeanFactory,BeanDefinition,BeanDefinitionReader 完成了IOC的初始化。

3、 Web IOC 容器

從大家最熟悉的 DispatcherServlet 開始,我們最先想到的還是 DispatcherServlet 的 init()方法。在 DispatherServlet 中並沒有找到 init()方法。往上追索在其父類HttpServletBean 中找到了我們想要的 init()方法,如下:

@Override
public final void init() throws ServletException {
   if (logger.isDebugEnabled()) {
      logger.debug("Initializing servlet '" + getServletName() + "'");
   }


   // Set bean properties from init parameters.
   PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
   if (!pvs.isEmpty()) {
      try {
         //定位資源
         BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
         //載入配置資訊
         ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
         bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
         initBeanWrapper(bw);
         bw.setPropertyValues(pvs, true);
      }
      catch (BeansException ex) {
         if (logger.isErrorEnabled()) {
            logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
         }
         throw ex;
      }
   }


   // Let subclasses do whatever initialization they like.
   initServletBean();


   if (logger.isDebugEnabled()) {
      logger.debug("Servlet '" + getServletName() + "' configured successfully");
   }
}

在init()方法中,真正完成初始化容器動作的邏輯其實在 initServletBean()方法中,繼續跟進initServletBean()中的程式碼在 FrameworkServlet 類中:

@Override
protected final void initServletBean() throws ServletException {
   getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
   if (this.logger.isInfoEnabled()) {
      this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
   }
   long startTime = System.currentTimeMillis();


   try {


      this.webApplicationContext = initWebApplicationContext();
      initFrameworkServlet();
   }
   catch (ServletException ex) {
      this.logger.error("Context initialization failed", ex);
      throw ex;
   }
   catch (RuntimeException ex) {
      this.logger.error("Context initialization failed", ex);
      throw ex;
   }


   if (this.logger.isInfoEnabled()) {
      long elapsedTime = System.currentTimeMillis() - startTime;
      this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
            elapsedTime + " ms");
   }
}

在上面的程式碼中終於看到了我們似曾相識的程式碼 initWebAppplicationContext(),繼續跟進:

protected WebApplicationContext initWebApplicationContext() {


   //先從ServletContext中獲得父容器WebApplicationContext
   WebApplicationContext rootContext =
         WebApplicationContextUtils.getWebApplicationContext(getServletContext());
   //宣告子容器
   WebApplicationContext wac = null;


   //建立父、子容器之間的關聯關係
   if (this.webApplicationContext != null) {
      // A context instance was injected at construction time -> use it
      wac = this.webApplicationContext;
      if (wac instanceof ConfigurableWebApplicationContext) {
         ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
         if (!cwac.isActive()) {
            // The context has not yet been refreshed -> provide services such as
            // setting the parent context, setting the application context id, etc
            if (cwac.getParent() == null) {
               // The context instance was injected without an explicit parent -> set
               // the root application context (if any; may be null) as the parent
               cwac.setParent(rootContext);
            }
            //這個方法裡面呼叫了AbstractApplicationContext的refresh()方法
            //模板方法,規定IOC初始化基本流程
            configureAndRefreshWebApplicationContext(cwac);
         }
      }
   }
   //先去ServletContext中查詢Web容器的引用是否存在,並建立好預設的空IOC容器
   if (wac == null) {
      // No context instance was injected at construction time -> see if one
      // has been registered in the servlet context. If one exists, it is assumed
      // that the parent context (if any) has already been set and that the
      // user has performed any initialization such as setting the context id
      wac = findWebApplicationContext();
   }
   //給上一步建立好的IOC容器賦值
   if (wac == null) {
      // No context instance is defined for this servlet -> create a local one
      wac = createWebApplicationContext(rootContext);
   }


   //觸發onRefresh方法
   if (!this.refreshEventReceived) {
      // Either the context is not a ConfigurableApplicationContext with refresh
      // support or the context injected at construction time had already been
      // refreshed -> trigger initial onRefresh manually here.
      onRefresh(wac);
   }


   if (this.publishContext) {
      // Publish the context as a servlet context attribute.
      String attrName = getServletContextAttributeName();
      getServletContext().setAttribute(attrName, wac);
      if (this.logger.isDebugEnabled()) {
         this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
               "' as ServletContext attribute with name [" + attrName + "]");
      }
   }


   return wac;
}

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
   if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
      // The application context id is still set to its original default value
      // -> assign a more useful id based on available information
      if (this.contextId != null) {
         wac.setId(this.contextId);
      }
      else {
         // Generate default id...
         wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
               ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
      }
   }


   wac.setServletContext(getServletContext());
   wac.setServletConfig(getServletConfig());
   wac.setNamespace(getNamespace());
   wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));


   // The wac environment's #initPropertySources will be called in any case when the context
   // is refreshed; do it eagerly here to ensure servlet property sources are in place for
   // use in any post-processing or initialization that occurs below prior to #refresh
   ConfigurableEnvironment env = wac.getEnvironment();
   if (env instanceof ConfigurableWebEnvironment) {
      ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
   }


   postProcessWebApplicationContext(wac);
   applyInitializers(wac);
   wac.refresh();
}

從上面的程式碼中可以看出,在 configAndRefreshWebApplicationContext()方法中,呼叫AbstractApplicationContext的 refresh()方法,這個是真正啟動 IOC 容器的入口。IOC 容器初始化以後,最後呼叫了DispatcherServlet 的 onRefresh()方法,在 onRefresh()方法中又是直接呼叫 initStrategies()方法初始化 SpringMVC 的九大元件:

@Override
protected void onRefresh(ApplicationContext context) {
   initStrategies(context);
}


/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
//初始化策略
protected void initStrategies(ApplicationContext context) {
   //多檔案上傳的元件
   initMultipartResolver(context);
   //初始化本地語言環境
   initLocaleResolver(context);
   //初始化模板處理器
   initThemeResolver(context);
   //handlerMapping
   initHandlerMappings(context);
   //初始化引數介面卡
   initHandlerAdapters(context);
   //初始化異常攔截器
   initHandlerExceptionResolvers(context);
   //初始化檢視前處理器
   initRequestToViewNameTranslator(context);
   //初始化檢視轉換器
   initViewResolvers(context);
   //
   initFlashMapManager(context);
}

Spring IOC容器初始化三部曲

在這裡插入圖片描述

基於XML的IOC容器初始化(定位、載入和註冊)

在這裡插入圖片描述
步驟: 一步一步手繪Spring IOC執行時序圖(基於XML的IOC容器初始化)

基於Annotation 的IOC容器初始化(定位、載入和註冊)

在這裡插入圖片描述
步驟: 一步一步手繪Spring IOC執行時序圖三(基於Annotation的IOC容器初始化)

4、IOC 容器初始化小結

通過上面的程式碼,總結一下 IOC 容器初始化的基本步驟:
1、初始化的入口在容器實現中的 refresh()呼叫來完成。
2、對 Bean 定義載入 IOC 容器使用的方法是 loadBeanDefinition()。
其中的大致過程如下:通過 ResourceLoader 來完成資原始檔位置的定位,DefaultResourceLoader是預設的實現,同時上下文字身就給出了 ResourceLoader 的實現,可以從類路徑,檔案系統,URL 等方式來定為資源位置。如果是 XmlBeanFactory 作為 IOC 容器,那麼需要為它指定 Bean 定義的資源,也就是說 Bean 定義檔案 時通過抽象成 Resource 來被 IOC 容器處理的 , 容器通過BeanDefinitionReader 來完成定義資訊的解析和 Bean 資訊的註冊, 往往使用的是 XmlBeanDefinitionReader 來解析 Bean 的 XML 定義檔案 - 實際的處理過程是委託給BeanDefinitionParserDelegate 來完成的,從而得到 bean 的定義資訊,這些資訊在 Spring 中使用
BeanDefinition物件來表示-這個名字可以讓我們想到loadBeanDefinition(),registerBeanDefinition()這些相關方法。它們都是為處理 BeanDefinitin 服務的,容器解析得到 BeanDefinition 以後,需要把它在 IOC 容器中註冊,這由 IOC 實現 BeanDefinitionRegistry 介面來實現。註冊過程就是在 IOC 容器內部維護的一個 HashMap 來儲存得到的 BeanDefinition 的過程。這個 HashMap 是 IOC 容器持有Bean 資訊的場所,以後對 Bean 的操作都是圍繞這個 HashMap 來實現的。
然後我們就可以通過 BeanFactory 和 ApplicationContext 來享受到 Spring IOC 的服務了,在使用 IOC容器的時候,我們注意到除了少量粘合程式碼,絕大多數以正確 IOC 風格編寫的應用程式程式碼完全不用關心如何到達工廠,因為容器將把這些物件與容器管理的其他物件鉤在一起。基本的策略是把工廠放到已知的地方,最好是放在對預期使用的上下文有意義的地方,以及程式碼將實際需要訪問工廠的地方。Spring本身提供了對宣告式載入 web 應用程式用法的應用程式上下文,並將其儲存在 ServletContext 中的框架實現。

以下是容器初始化全過程的時序圖:
在這裡插入圖片描述

相關文章