Spring原始碼閱讀-IoC容器解析

張風閒發表於2019-06-30

在Spring框架中最重要的是Spring IoC容器,它是Spring框架的核心。本文將從更高的角度來解析Sping IoC容器,瞭解其是如何設計的。瞭解一樣東西最好的辦法是從其核心本質出發,只要把握住了這樣一個核心,其他的一些東西也迎刃而解了。這是一個很好的開端,我們一起開始吧...

Spring IoC容器

org.springframework.context.ApplicationContext介面代表Spring IoC容器,主要負責bean的例項化、配置、裝配,簡而言之,Spring IoC容器是管理這些bean的(這裡所說的bean指的是組成你的應用程式中的物件,並且這些物件被Spring所管理)。容器如何知道哪些物件要進行例項化、配置和裝配的呢?是通過讀取配置檔案後設資料來達到這個效果的,配置檔案後設資料是用xml配置、Java註解和Java程式碼配置來表示的。這使得作為程式設計師的我們,只需要向Spring容器提供配置後設資料,Spring容器就能在我們的應用中例項化、配置和裝配這些物件。org.springframework.beansorg.springframework.context包是Spring IoC容器的基礎。Spring提供了很多Application介面的實現。在單獨的應用中,建立ClassPathXmlApplicationContextFileSystemXmlApplicationContext的例項是非常常用的做法。示例如下:

ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
Hello hello = (Hello) ac.getBean("hello");
hello.sayHello();

然而在大部分的應用場景中,不需要例項化一個或者多個Spring IoC容器的例項。例如在web應用的場景下,只需要在web.xml中建立七行樣板配置的程式碼如下:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</paramvalue>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

下面這張圖從更高的視角展示了Spring是怎樣工作的。你的應用程式中的類是和配置後設資料組合在一起,以便在ApplicationContext建立和初始化之後,你擁有了一個完全配置的、可執行的系統。

Spring原始碼閱讀-IoC容器解析

ApplicationContext設計解析

為了方便對ApplicationContext介面的層次結構有一個大概的認識,下面使用IDEA來生成ApplicationContext的繼承關係圖。(選中ApplicationContext介面->右鍵->Diagrams->Show Diagrams...)

(溫馨提示:點選圖片可以檢視高清大圖)

Spring原始碼閱讀-IoC容器解析

從上圖就能很清楚的看出ApplicationContext繼承的介面分為五類:

  • BeanFactory:提供了能夠管理任何物件的高階配置機制,這個介面是Spring框架中比較重要的一個介面。
    • ListableBeanFactory:從該介面的名字就能知道,該介面除了擁有BeanFactory的功能外,該介面還有能列出factory中所有bean的例項的能力。
    • HierarchicalBeanFactory:該介面除了擁有BeanFactory的功能外,還提供了BeanFactory分層的機制,查詢bean的時候,除了在自身BeanFactory查詢外,如果沒有查詢到,還會在父級BeanFactory進行查詢。
  • MessageSource:訊息資源的處理,用於國際化。
  • ApplicationEventPublisher:用於處理事件釋出機制。
  • EnvironmentCapable:提供了Environment的訪問能力。
  • ResourceLoader:用於載入資源的策略介面(例如類路徑下的資源、系統檔案下的資源等等)。
    • ResourcePatternResolver:用於將位置模式(例如Ant風格的路徑模式)解析成資源物件的策略介面。classpath*:字首能匹配所以類路徑下的資源。

先看一下在ApplicationContext中定義的方法:

String getId(); // 獲取ApplicationContext的唯一id
String getApplicationName(); // 該上下文所屬的已經部署了的應用的名字,預設為""
String getDisplayName(); // 友好的展示名字
long getStartupDate(); // 該上下文第一次載入的時間

ApplicationContext getParent(); // 父級ApplicationContext
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;

前四個方法用於獲取該ApplicationContext的一些基本資訊,getAutowireCapableBeanFactory()用於暴露AutowireCapableBeanFactory的功能,這通常不是提供給用於程式碼使用的,除非你想要在應用上下文的外面初始化bean的例項,關於AutowireCapableBeanFactory後面會有更加詳細的解析。

BeanFactory

BeanFactory是Spring框架中比較重要的一個介面,下面列出了這個介面中的方法的定義:

// 獲取bean
Object getBean(String name) throws BeansException;
<T> T getBean(String name, 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的提供者(物件工廠)
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

boolean containsBean(String name); // 是否包含指定名字的bean
boolean isSingleton(String name) throws NoSuchBeanDefinitionException; // 是否為單例
boolean isPrototype(String name) throws NoSuchBeanDefinitionException; // 是否為原型
// 指定名字的bean是否和指定的型別匹配
boolean isTypeMatch(String name, ResolvableType typeToMatch);
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
Class<?> getType(String name) 
    throws NoSuchBeanDefinitionException; // 獲取指定名字的bean的型別
String[] getAliases(String name); // 獲取指定名字的bean的所有別名

這些方法大致可以分為三類:

  • getBean()方法用於獲取匹配的bean的例項物件(有可能是Singleton或者Prototype的),如果沒有找到匹配的bean則丟擲BeansException子類的異常。如果在當前的工廠例項中沒有找到匹配的bean,會在父級的工廠中進行查詢。帶有args引數的getBean()方法,允許顯式的去指定構造器或者工廠方法的引數,會覆蓋了在bean的定義中定義的引數,這僅僅在建立一個新的例項的時候才起作用,而在獲取一個已經存在的例項是不起作用的。

  • getBeanProvider()方法用於獲取指定bean的提供者,可以看到它返回的是一個ObjectProvider,其父級介面是ObjectFactory。首先來看一下ObjectFactory,它是一個物件的例項工廠,只有一個方法:

    T getObject() throws BeansException;

    呼叫這個方法返回的是一個物件的例項。此介面通常用於封裝一個泛型工廠,在每次呼叫的時候返回一些目標物件新的例項。ObjectFactoryFactoryBean是類似的,只不過FactoryBean通常被定義為BeanFactory中的服務提供者(SPI)例項,而ObjectFactory通常是以API的形式提供給其他的bean。簡單的來說,ObjectFactory一般是提供給開發者使用的,FactoryBean一般是提供給BeanFactory使用的。

    ObjectProvider繼承ObjectFactory,特為注入點而設計,允許可選擇性的程式設計和寬泛的非唯一性的處理。在Spring 5.1的時候,該介面從Iterable擴充套件,提供了對Stream的支援。該介面的方法如下:

    // 獲取物件的例項,允許根據顯式的指定構造器的引數去構造物件
    T getObject(Object... args) throws BeansException;
    // 獲取物件的例項,如果不可用,則返回null
    T getIfAvailable() throws BeansException;
    T getIfAvailable(Supplier<T> defaultSupplier) throws BeansException;
    void ifAvailable(Consumer<T> dependencyConsumer) throws BeansException;
    // 獲取物件的例項,如果不是唯一的或者沒有首先的bean,則返回null
    T getIfUnique() throws BeansException;
    T getIfUnique(Supplier<T> defaultSupplier) throws BeansException;
    void ifUnique(Consumer<T> dependencyConsumer) throws BeansException;
    
    // 獲取多個物件的例項
    Iterator<T> iterator();
    Stream<T> stream();
    Stream<T> orderedStream()

    這些介面是分為兩類,

    • 一類是獲取單個物件,getIfAvailable()方法用於獲取可用的bean(沒有則返回null),getIfUnique()方法用於獲取唯一的bean(如果bean不是唯一的或者沒有首選的bean返回null)。getIfAvailable(Supplier<T> defaultSupplier)getIfUnique(Supplier<T> defaultSupplier),如果沒有獲取到bean,則返回defaultSupplier提供的預設值,ifAvailable(Consumer<T> dependencyConsumer)ifUnique(Consumer<T> dependencyConsumer)提供了以函數語言程式設計的方式去消費獲取到的bean。
    • 另一類是獲取多個物件,stream()方法返回連續的Stream,不保證bean的順序(通常是bean的註冊順序)。orderedStream()方法返回連續的Stream,預先會根據工廠的公共排序比較器進行排序,一般是根據org.springframework.core.Ordered的約定進行排序。
  • 其他的是一些工具性的方法:

    • 通過名字判斷是否包含指定bean的定義的containsBean(String name)方法
    • 判斷是單例和原型的isSingleton(String name)isPrototype(String name)方法
    • 判斷給定bean的名字是否和型別匹配的isTypeMatch方法
    • 根據bean的名字來獲取其型別的getType(String name)方法
    • 根據bean的名字來獲取其別名的getAliases(String name)方法

或許你已經注意到了,有兩個方法含有型別是ResolvableType的引數,那麼ResolvableType是什麼呢?假如說你要獲取泛型型別的bean:MyBean<TheType>,根據Class來獲取,肯定是滿足不了要求的,泛型在編譯時會被擦除。使用ResolvableType就能滿足此需求,程式碼如下:

ResolvableType type = ResolvableType.forClassWithGenerics(MyType.class, TheType.class);
ObjectProvider<MyType<TheType>> op = applicationContext.getBeanProvider(type);
MyType<TheType> bean = op.getIfAvailable()

簡單的來說,ResolvableType是對Java java.lang.reflect.Type的封裝,並且提供了一些訪問該型別的其他資訊的方法(例如父類, 泛型引數,該類)。從成員變數、方法引數、方法返回型別、類來構建ResolvableType的例項。

ListableBeanFactory

ListableBeanFactory介面有能列出工廠中所有的bean的能力,下面給出該介面中的所有方法:

boolean containsBeanDefinition(String beanName); // 是否包含給定名字的bean的定義
int getBeanDefinitionCount(); // 工廠中bean的定義的數量
String[] getBeanDefinitionNames(); // 工廠中所有定義了的bean的名字
// 獲取指定型別的bean的名字
String[] getBeanNamesForType(ResolvableType type);
String[] getBeanNamesForType(Class<?> type);
String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit);
// 獲取所有使用提供的註解進行標註的bean的名字
String[] getBeanNamesForAnnotation(Class<? extends Annotation> annotationType);
// 查詢指定bean中的所有指定的註解(會考慮介面和父類中的註解)
<A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType)
    throws NoSuchBeanDefinitionException;

// 根據指定的型別來獲取所有的bean
<T> Map<String, T> getBeansOfType(Class<T> type) throws BeansException;
<T> Map<String, T> getBeansOfType(Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
    throws BeansException;
// 獲取所有使用提供的註解進行標註了的bean
Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType) throws BeansException;

上面的這些方法都不考慮祖先工廠中的bean,只會考慮在當前工廠中定義的bean。

  • 前八個方法用於獲取bean的一些資訊
  • 最後的三個方法用於獲取所有滿足條件的bean,返回結果Map中的鍵為bean的名字,值為bean的例項。這些方法都會考慮通過FactoryBean建立的bean,這也意味著FactoryBean會被初始化。為什麼這裡的三個方法不返回List?Map不光包含這些bean的例項,而且還包含bean的名字,而List只包含bean的例項。也就是說Map比List更加的通用。

HierarchicalBeanFactory

HierarchicalBeanFactory介面定義了BeanFactory之間的分層結構,ConfigurableBeanFactory中的setParentBeanFactory方法能設定父級的BeanFactory,下面列出了HierarchicalBeanFactory中定義的方法:

BeanFactory getParentBeanFactory(); // 獲取父級的BeanFactory
// 本地的工廠是否包含指定名字的bean
boolean containsLocalBean(String name);

這兩個方法都比較直接明瞭,getParentBeanFactory方法用於獲取父級BeanFactorycontainsLocalBean

用於判斷本地的工廠是否包含指定的bean,忽略在祖先工廠中定義的bean。

MessageSource

MessageSource主要用於訊息的國際化,下面是該介面中的方法定義:

// 獲取訊息
String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

以上的三個方法都是用於獲取訊息的,第一個方法提供了預設訊息,第二個介面如果沒有獲取到指定的訊息會丟擲異常。第三個介面中的MessageSourceResolvable引數是對程式碼、引數值、預設值的一個封裝。

ApplicationEventPublisher

ApplicationEventPublisher介面封裝了事件釋出功能,提供Spring中事件的機制。介面中的方法定義如下:

// 釋出事件
void publishEvent(ApplicationEvent event);
void publishEvent(Object event);

第一個方法用於釋出特定於應用程式事件。第二個方法能釋出任意的事件,如果事件不是ApplicationEvent,那麼會被包裹成PayloadApplicationEvent事件。

EnvironmentCapable

EnvironmentCapable提供了訪問Environment的能力,該介面只有一個方法:

Environment getEnvironment();

Environment表示當前正在執行的應用的環境變數,它分為兩個部分:profiles和properties。它的父級介面PropertyResolver提供了property的訪問能力。

ResourceLoader和ResourcePatternResolver

先來看一下ResourceLoader,該介面是用來載入資源(例如類路徑或者檔案系統中的資源)的策略介面。該介面中的方法如下:

Resource getResource(String location); // 根據指定的位置獲取資源
ClassLoader getClassLoader(); // 獲取該資源載入器所使用的類載入器

該介面只有簡單明瞭的兩個方法,一個是用來獲取指定位置的資源,一個用於獲取資源載入器所使用的類載入器。Resource是從實際型別的底層資源(例如檔案、類路徑資源)進行抽象的資源描述符。先看下Resource中的方法:


boolean exists(); // 資源實際上是否存在
boolean isReadable(); // 資源是否可讀
boolean isOpen(); // 檢查資源是否為開啟的流
boolean isFile(); // 資源是否為檔案系統上的一個檔案

URL getURL() throws IOException; // 獲取url
URI getURI() throws IOException; // 獲取URI
File getFile() throws IOException; // 獲取檔案

ReadableByteChannel readableChannel() throws IOException; // 獲取ReadableByteChannel

long contentLength() throws IOException; // 資源的內容的長度
long lastModified() throws IOException; // 資源的最後修改時間
// 相對於當前的資源建立一個新的資源
Resource createRelative(String relativePath) throws IOException; 
String getFilename(); // 獲取資源的檔名
String getDescription(); // 獲取資源的描述資訊

Resource的父級介面InputStreamSource,可以簡單的理解為InputStream的來源,只有一個方法,如下:

InputStream getInputStream() throws IOException; // 獲取輸入流

接下來在來看一下ResourcePatternResolver,該介面用於解析一個位置模式(例如Ant風格的路徑模式),該介面只有一個方法,如下:

// 將給定的位置模式解析成資源物件
Resource[] getResources(String locationPattern) throws IOException;

Spring IoC容器設計覆盤

假如讓你設計IoC容器,你該如何去做呢?首先你應該要明確你設計的容器的功能和特性,然後根據這些功能和特性設計出合理的介面。下面只是粗略的分析一下:

  • IoC容器對bean的配置和管理,那麼是不是需要設計一個介面來完成這些功能呢?(BeanFactory)
  • 既然需要這些後設資料的配置,那麼是不是需要設計一個介面來完成對一些配置檔案的讀取。(ResourceLoader和Resource)
  • 在IoC容器初始化、摧毀的時候,是不是可能要執行一些操作呢?那麼是不是需要使用事件機制來完成呢?(ApplicationEventPublisher)
  • ....

本文思維導圖

Spring原始碼閱讀-IoC容器解析

相關文章