Spring原始碼閱讀-ApplicationContext體系結構分析

張風閒發表於2019-07-15

上篇已經對IoC容器的設計進行了分析(Spring原始碼閱讀-IoC容器解析),本篇將對ApplicationContext經典的繼承層次圖進行詳細的分析,在心中形成一個大致的印象,以便後面一步步除錯原始碼的時候,不會太眼花繚亂。讓我們一步步的前進吧...

繼承層次圖概覽

使用IDEA的繼承層次工具生成如下的圖(選中ApplicationContext --> Ctrl+H):

(溫馨提示:雙擊可檢視高清大圖)

Spring原始碼閱讀-ApplicationContext體系結構分析

從上圖能很清楚的看出,ApplicationContext的子介面分為兩個部分:

  • ConfigurableApplicationContext:大部分的應用上下文都實現了該介面
  • WebApplicationContext:在web應用程式中使用

ConfigurableApplicationContext分析

從上面的類的繼承層次圖能看到,ConfigurableApplicationContext是比較上層的一個介面,該介面也是比較重要的一個介面,幾乎所有的應用上下文都實現了該介面。該介面在ApplicationContext的基礎上提供了配置應用上下文的能力,此外提供了生命週期的控制能力。先看一下該介面的繼承關係圖(為了更加簡潔,去掉了ApplicationContext繼承的介面):

(溫馨提示:雙擊可檢視高清大圖)

Spring原始碼閱讀-ApplicationContext體系結構分析

Closeable介面用於關閉應用上下文,釋放所有的資源和鎖,這也包括摧毀所有快取的單例的bean,常見的try-with-resources用法如下,執行完try體中的程式碼後會自動的呼叫close方法:

try (ConfigurableApplicationContext cac = ...) {
    // 編寫程式碼 
    ...
}

Lifecycle定義了啟動/停止生命週期的控制的一些方法,其中的方法如下:

void start(); // 啟動元件
void stop(); // 停止元件
boolean isRunning(); // 元件是否正在執行

接下來看一下ConfigurableApplicationContext中的方法:

void setId(String id); // 設定應用上下文唯一的id
void setParent(ApplicationContext parent); // 設定應用程式上下文的父級
void setEnvironment(ConfigurableEnvironment environment); // 設定應用上下文的環境
ConfigurableEnvironment getEnvironment();  // 獲取應用上下文的環境
// 新增一個新的BeanFactoryPostProcessor
void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor);
// 新增應用程式監聽器
void addApplicationListener(ApplicationListener<?> listener);
// 新增協議解析器,可能會覆蓋預設的規則
void addProtocolResolver(ProtocolResolver resolver);
// 載入或者重新整理配置
void refresh() throws BeansException, IllegalStateException;
// 向JVM runtime註冊一個關閉鉤子,JVM關閉時關閉這個上下文
void registerShutdownHook();
// 應用程式上問下是否是啟用狀態
boolean isActive();
// 獲取應用上下文內部的bean factory
ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

上面的這些方法基本上是提供了對某些特性的實現進行支撐的方法。

看了這麼多方法,下面看一下ApplicationContext的抽象的實現。

AbstractApplicationContext

AbstractApplicationContextApplicationContext介面的抽象實現,這個抽象類僅僅是實現了公共的上下文特性。這個抽象類使用了模板方法設計模式,需要具體的實現類去實現這些抽象的方法。對相關介面的實現如下:

  • ApplicationContext介面的實現
  • ConfigurableApplicationContext介面的實現
  • BeanFactory介面的實現
  • ListableBeanFactory介面的實現
  • HierarchicalBeanFactory介面的實現
  • MessageSource介面的實現
  • ResourcePatternResolver的實現
  • Lifecycle介面的實現

本文不會詳細的講解這個類中的具體的實現細節,後面會有更加的詳細的介紹。下面看下里面的抽象方法:

// 重新整理BeanFactory,用於執行實際的配置載入,該方法在其他的初始化工作之前被refresh()方法呼叫
protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
// 關閉BeanFactory,用於釋放內部使用的BeanFactory·
protected abstract void closeBeanFactory();
// 獲取內部使用的BeanFactory
public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

那麼對需要實現的方法經過抽象後,只剩下少量的需要子類去實現的方法。

GenericApplicationContext

GenericApplicationContext繼承自AbstractApplicationContext,是為通用目的設計的,它能載入各種配置檔案,例如xml,properties等等。它的內部持有一個DefaultListableBeanFactory的例項,實現了BeanDefinitionRegistry介面,以便允許向其應用任何bean的定義的讀取器。為了能夠註冊bean的定義,refresh()只允許呼叫一次。常見的使用如下:

GenericApplicationContext ctx = new GenericApplicationContext();
XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx);
xmlReader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml"));
PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx);
propReader.loadBeanDefinitions(new ClassPathResource("otherBeans.properties"));
ctx.refresh();

MyBean myBean = (MyBean) ctx.getBean("myBean");
..

這個類的實現沒有太多需要注意的地方,需要注意的有兩點:

  • 內部使用的DefaultListableBeanFactory的例項,提供了一些方法來配置該例項,例如是否允許bean定義的覆蓋、是否允許bean之間的迴圈應用等等。
  • 該類實現了BeanDefinitionRegistry,bean的定義註冊。以便能通過BeanDefinitionReader讀取bean的配置,並註冊。BeanDefinitionRegistry介面的實現是直接使用內部的DefaultListableBeanFactory的例項。

GenericXmlApplicationContext

GenericXmlApplicationContext繼承自GenericApplicationContext,內建了對XML的支援。它非常的方便和靈活,是ClassPathXmlApplicationContextFileSystemXmlApplicationContext的一種替代品。可以發現,它的內部有一個XmlBeanDefinitionReader的例項,專門用於處理XML的配置。

StaticApplicationContext

StaticApplicationContext繼承自GenericApplicationContext,主要用於程式設計式的注入bean和訊息,而不是從外部的配置源讀取bean的定義。主要是在測試時非常有用。通過閱讀原始碼可以看到,它的內部有一個StaticMessageSource的例項,使用addMessage方法新增訊息。每次在程式設計式的注入bean時,都會建立一個GenericBeanDefinition的例項。

ResourceAdapterApplicationContext

ResourceAdapterApplicationContext繼承自GenericApplicationContext,是為JCA(J2EE Connector Architecture)的ResourceAdapter設計的,主要用於傳遞BootstrapContext的例項給實現了BootstrapContextAware介面且由spring管理的bean。覆蓋了postProcessBeanFactory方法來實現此功能。

GenericGroovyApplicationContext

GenericGroovyApplicationContext繼承自GenericApplicationContext,實現了GroovyObject介面以便能夠使用點的語法(.xx)取代getBean方法來獲取bean。它主要用於Groovy bean的定義,與GenericXmlApplicationContext一樣,它也能解析XML格式定義的bean。內部使用GroovyBeanDefinitionReader來完成groovy指令碼和XML的解析。

AnnotationConfigApplicationContext

AnnotationConfigApplicationContext繼承自GenericApplicationContext,提供了註解配置(例如:Configuration、Component、inject等)和類路徑掃描(scan方法)的支援,可以使用register(Class<?>... annotatedClasses)來註冊一個一個的進行註冊。實現了AnnotationConfigRegistry介面,來完成對註冊配置的支援,只有兩個方法:register和scan。內部使用AnnotatedBeanDefinitionReader來完成註解配置的解析,使用ClassPathBeanDefinitionScanner來完成類路徑下的bean定義的掃描。

AbstractRefreshableApplicationContext

AbstractRefreshableApplicationContext繼承自AbstractApplicationContext,支援多次進行重新整理(多次呼叫refresh方法),每次重新整理時在內部建立一個新的bean工廠的例項。子類僅僅需要實現loadBeanDefinitions方法,該方法在每次重新整理時都會呼叫。

AbstractRefreshableConfigApplicationContext

AbstractRefreshableConfigApplicationContext繼承自AbstractRefreshableApplicationContext,新增了對指定的配置檔案路徑的公共的處理,可以把他看作基於XML的應用上下文的基類。實現瞭如下的兩個介面:

  • BeanNameAware用於設定上下文的bean的名稱,只有一個方法:void setBeanName(String name)
  • InitializingBean用於上下文一切就緒後,如果還未重新整理,那麼就執行重新整理操作,只有一個方法:void afterPropertiesSet()

AbstractXmlApplicationContext

AbstractXmlApplicationContext繼承自AbstractRefreshableConfigApplicationContext,用於描繪包含能被XmlBeanDefinitionReader所理解的bean定義的XML文件。子類只需要實現getConfigResourcesgetConfigLocations來提供配置檔案資源。

FileSystemXmlApplicationContext

FileSystemXmlApplicationContext繼承自AbstractXmlApplicationContext,用於解析檔案系統中XML配置檔案。檔案的路徑可以是具體的檔案路徑,例如:xxx/application.xml,也可以是ant風格的配置,例如:xxx/*-context.xml。

看下面的通過檔案路徑來獲取資源的程式碼:

@Override
protected Resource getResourceByPath(String path) {
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    return new FileSystemResource(path);
}

檔案路徑前面的/會被去掉,無論是否路徑前面是否加上/,檔案路徑都會解析成相對路徑,即基於JVM的當前工作路徑。獲取到的資源物件是FileSystemResource,用於處理檔案系統相關的資源。

ClassPathXmlApplicationContext

ClassPathXmlApplicationContext繼承自AbstractXmlApplicationContext,和FileSystemXmlApplicationContext類似,只不過ClassPathXmlApplicationContext是用於處理類路徑下的XML配置檔案。檔案的路徑可以是具體的檔案路徑,例如:xxx/application.xml,也可以是ant風格的配置,例如:xxx/*-context.xml。

WebApplicationContext

該介面提供了在web應用中的配置,介面提供了一個ServletContext getServletContext()用來獲取ServletContext物件。該介面會和ServletContext的一個屬性進行繫結,這個屬性就是ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE。定義了三個作用域的名稱:SCOPE_REQUEST,SCOPE_SESSION,SCOPE_APPLICATION。在工廠中的bean的名稱:SERVLET_CONTEXT_BEAN_NAME。ServletContext初始化引數名稱:CONTEXT_PARAMETERS_BEAN_NAME。在工廠中ServletContext屬性值環境bean的名稱:CONTEXT_ATTRIBUTES_BEAN_NAME

ConfigurableWebApplicationContext

ConfigurableWebApplicationContext繼承自WebApplicationContextConfigurableApplicationContext,提供了web應用上下文的可配置的能力。相關介面定義如下:

// 設定web應用上下文的ServletContext
void setServletContext(@Nullable ServletContext servletContext);
// 設定web應用上下文的ServletConfig
void setServletConfig(@Nullable ServletConfig servletConfig);
// 獲取web應用上下文的ServletConfig
ServletConfig getServletConfig();
// 設定web應用上下文的名稱空間
void setNamespace(@Nullable String namespace);
// 獲取web應用上下文的名稱空間
String getNamespace();
// 以初始化引數的形式設定web應用上下文的配置檔案位置
void setConfigLocation(String configLocation);
// 設定web應用上下文的配置檔案的位置
void setConfigLocations(String... configLocations);
// 獲取web應用上下文的配置檔案位置
String[] getConfigLocations();

上面的介面主要都是一些設定或者獲取的方法,在web應用上下文中需要用到的一些東西。

GenericWebApplicationContext

GenericWebApplicationContext繼承自GenericApplicationContext,實現了ConfigurableWebApplicationContextThemeSource介面。該類設計的目的不是在web.xml中進行宣告式的安裝,而是程式設計式的安裝,例如使用WebApplicationInitializers來構建內嵌的上下文。該介面在ConfigurableWebApplicationContext的內容都是一個偽實現,呼叫其中的大多數方法都會丟擲異常。你也許注意到了,他實現了ThemeSource介面,那麼他有什麼用呢?字面意思是主題源,它設計的目的主要是用於訊息的國際化。

StaticWebApplicationContext

StaticWebApplicationContext繼承自StaticApplicationContext實現了ConfigurableWebApplicationContextThemeSource介面。該介面主要是用在測試的環境,不用於產品環境。

AbstractRefreshableWebApplicationContext

GenericWebApplicationContext繼承自AbstractRefreshableConfigApplicationContext,實現了ConfigurableWebApplicationContextThemeSource介面,主要是用於web環境下。在web程式啟動的時候,提供一個configLocations屬性,通過ConfigurableWebApplicationContext介面來進行填充。子類化這個介面是很簡單的,所有你所需要做的事情就是實現loadBeanDefinitions方法,來實現你自己的bean定義的載入邏輯。

XmlWebApplicationContext

XmlWebApplicationContext繼承自AbstractRefreshableWebApplicationContext,接受能被XmlBeanDefinitionReader所理解的XML文件配置。對於根上下文,預設的配置檔案路徑是/WEB-INF/applicationContext.xml,對於名稱空間為test-servlet的上下文,預設的配置檔案路徑是/WEB-INF/test-servlet.xml(就像servlet-name為test的DispatcherServlet例項)。

預設的配置檔案路徑處理的程式碼如下:

protected String[] getDefaultConfigLocations() {
    if (getNamespace() != null) {
        return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
    }
    else {
        return new String[] {DEFAULT_CONFIG_LOCATION};
    }
}

和其他的上下文一樣,bean定義的載入也是在void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)方法中,使用的是XmlBeanDefinitionReader

GroovyWebApplicationContext

GroovyWebApplicationContext繼承自AbstractRefreshableWebApplicationContext,實現了GroovyObject介面,接受能被GroovyBeanDefinitionReader所理解的groovy bean定義指令碼和XML文件配置。對於web環境,基本上是和GenericGroovyApplicationContext是等價的。對於根上下文,預設的配置檔案路徑是/WEB-INF/applicationContext.groovy,對於名稱空間為test-servlet的上下文,預設的配置檔案路徑是/WEB-INF/test-servlet.xml(就像servlet-name為test的DispatcherServlet例項)。

預設的配置檔案路徑處理的程式碼如下:

protected String[] getDefaultConfigLocations() {
    if (getNamespace() != null) {
        return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
    }
    else {
        return new String[] {DEFAULT_CONFIG_LOCATION};
    }
}

和其他的上下文一樣,bean定義的載入也是在void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)方法中,使用的是GroovyBeanDefinitionReader

AnnotationConfigWebApplicationContext

AnnotationConfigWebApplicationContext繼承自AbstractRefreshableWebApplicationContext

接受註解的類作為輸入(特殊的@Configuration註解類,一般的@Component註解類,與JSR-330相容的javax.inject註解)。允許一個一個的注入,同樣也能使用類路徑掃描。對於web環境,基本上是和AnnotationConfigApplicationContext等價的。使用AnnotatedBeanDefinitionReader來對註解的bean進行處理,使用ClassPathBeanDefinitionScanner來對類路徑下的bean進行掃描。

部分程式碼如下:

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) {
    AnnotatedBeanDefinitionReader reader = getAnnotatedBeanDefinitionReader(beanFactory);
    ClassPathBeanDefinitionScanner scanner = getClassPathBeanDefinitionScanner(beanFactory);
    ...
    if (!this.annotatedClasses.isEmpty()) {
       ....
        reader.register(ClassUtils.toClassArray(this.annotatedClasses));
    }

    if (!this.basePackages.isEmpty()) {
        ....
        scanner.scan(StringUtils.toStringArray(this.basePackages));
    }

    String[] configLocations = getConfigLocations();
    if (configLocations != null) {
        for (String configLocation : configLocations) {
            try {
                Class<?> clazz = ClassUtils.forName(configLocation, getClassLoader());
                if (logger.isTraceEnabled()) {
                    logger.trace("Registering [" + configLocation + "]");
                }
                reader.register(clazz);
            }
            catch (ClassNotFoundException ex) {
                ....
                }
            }
        }
    }
}

本文思維導圖

Spring原始碼閱讀-ApplicationContext體系結構分析

相關文章