Spring IOC 容器源碼分析系列文章導讀

1. 簡介

Spring 是一個輕量級的企業級應用開發框架,於 2004 年由 Rod Johnson 發佈了 1.0 版本。通過十幾年的迭代,如今的 Spring 框架已經很是成熟了。Spring 包含了衆多模塊,包括但不限於 Core、Bean、Context、AOP 和 Web 等。在今天,咱們徹底可使用 Spring 所提供的一站式解決方案開發出咱們所須要的應用。做爲 Java 程序員,咱們會常常和 Spring 框架打交道,因此仍是頗有必要弄懂 Spring 的原理。mysql

本文是 Spring IOC 容器源碼分析系列文章的第一篇文章,將會着重介紹 Spring 的一些使用方法和特性,爲後續的源碼分析文章作鋪墊。另外須要特別說明一下,本系列的源碼分析文章是基於Spring 4.3.17.RELEASE版本編寫的,而非最新的5.0.6.RELEASE版本。好了,關於簡介先說到這裏,繼續來講說下面的內容。git

 2. 文章編排

寫 Spring IOC 這一塊的文章,挺讓我糾結的。我本來是打算在一篇文章中分析全部的源碼,可是後來發現文章實在太長。主要是由於 Spring IOC 部分的源碼實在太長,將這一部分的源碼貼在一篇文章中仍是很壯觀的。固然估計你們也沒興趣讀下去,因此決定對文章進行拆分。這裏先貼一張文章切分前的目錄結構:程序員

如上圖,由目錄能夠看出,假使在一篇文章中寫完全部內容,文章的長度將會很是長。因此在通過思考後,我會將文章拆分紅一系列的文章,以下:github

  1. Spring IOC 容器源碼分析 - 獲取單例 bean - ✅ 已更新
  2. Spring IOC 容器源碼分析 - 建立單例 bean 的過程 - ✅ 已更新
  3. Spring IOC 容器源碼分析 - 建立原始 bean 對象 - ✅ 已更新
  4. Spring IOC 容器源碼分析 - 循環依賴的解決辦法 - ✅ 已更新
  5. Spring IOC 容器源碼分析 - 填充屬性到原始 bean 對象中 - ✅ 已更新
  6. Spring IOC 容器源碼分析 - 餘下的初始化工做 - ✅ 已更新

上面文章對應的源碼分析工做均已經完成,全部的文章將會在近期內進行更新。spring

 3. Spring 模塊結構

Spring 是分模塊開發的,Spring 包含了不少模塊,其中最爲核心的是 bean 容器相關模塊。像 AOP、MVC、Data 等模塊都要依賴 bean 容器。這裏先看一下 Spring 框架的結構圖:sql

圖片來源:Spring 官方文檔segmentfault

從上圖中能夠看出Core Container處於整個框架的最底層(忽略 Test 模塊),在其之上有 AOP、Data、Web 等模塊。既然 Spring 容器是最核心的部分,那麼你們若是要讀 Spring 的源碼,容器部分必須先弄懂。本篇文章做爲 Spring IOC 容器的開篇文章,就來簡單介紹一下容器方面的知識。請繼續往下看。app

 4. Spring IOC 部分特性介紹

本章將會介紹 IOC 中的部分特性,這些特性均會在後面的源碼分析中悉數到場。若是你們不是很熟悉這些特性,這裏能夠看一下。框架

 4.1 alias

alias 的中文意思是「別名」,在 Spring 中,咱們可使用 alias 標籤給 bean 起個別名。好比下面的配置:ide

1
2
3
4
5
<bean id="hello" class="xyz.coolblog.service.Hello">
    <property name="content" value="hello"/>
</bean>
<alias name="hello" alias="alias-hello"/>
<alias name="alias-hello" alias="double-alias-hello"/>

這裏咱們給hello這個 beanName 起了一個別名alias-hello,而後又給別名alias-hello起了一個別名double-alias-hello。咱們能夠經過這兩個別名獲取到hello這個 bean 實例,好比下面的測試代碼:

1
2
3
4
5
6
7
8
9
10
public class ApplicationContextTest {

    @Test
    public void testAlias() {
        String configLocation = "application-alias.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
        System.out.println("    alias-hello -> " + applicationContext.getBean("alias-hello"));
        System.out.println("double-alias-hello -> " + applicationContext.getBean("double-alias-hello"));
    }
}

測試結果以下:

 4.2 autowire

本小節,咱們來了解一下 autowire 這個特性。autowire 即自動注入的意思,經過使用 autowire 特性,咱們就不用再顯示的配置 bean 之間的依賴了。把依賴的發現和注入都交給 Spring 去處理,省時又省力。autowire 幾個可選項,好比 byName、byType 和 constructor 等。autowire 是一個經常使用特性,相信你們都比較熟悉了,因此本節咱們就 byName 爲例,快速結束 autowire 特性的介紹。

當 bean 配置中的 autowire = byName 時,Spring 會首先經過反射獲取該 bean 所依賴 bean 的名字(beanName),而後再經過調用 BeanFactory.getName(beanName) 方法便可獲取對應的依賴實例。autowire = byName 原理大體就是這樣,接下來咱們來演示一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Service {

    private Dao mysqlDao;

    private Dao mongoDao;

    // 忽略 getter/setter

    @Override
    public String toString() {
        return super.toString() + "\n\t\t\t\t\t{" +
            "mysqlDao=" + mysqlDao +
            ", mongoDao=" + mongoDao +
            '}';
    }
}

public interface Dao {}
public class MySqlDao implements Dao {}
public class MongoDao implements Dao {}

配置以下:

1
2
3
4
5
6
7
8
9
10
11
<bean name="mongoDao" class="xyz.coolblog.autowire.MongoDao"/>
<bean name="mysqlDao" class="xyz.coolblog.autowire.MySqlDao"/>

<!-- 非自動注入,手動配置依賴 -->
<bean name="service-without-autowire" class="xyz.coolblog.autowire.Service" autowire="no">
    <property name="mysqlDao" ref="mysqlDao"/>
    <property name="mongoDao" ref="mongoDao"/>
</bean>

<!-- 經過設置 autowire 屬性,咱們就不須要像上面那樣顯式配置依賴了 -->
<bean name="service-with-autowire" class="xyz.coolblog.autowire.Service" autowire="byName"/>

測試代碼以下:

1
2
3
4
String configLocation = "application-autowire.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
System.out.println("service-without-autowire -> " + applicationContext.getBean("service-without-autowire"));
System.out.println("service-with-autowire -> " + applicationContext.getBean("service-with-autowire"));

測試結果以下:

從測試結果能夠看出,兩種方式配置方式都能完成解決 bean 之間的依賴問題。只不過使用 autowire 會更加省力一些,配置文件也不會冗長。這裏舉的例子比較簡單,假使一個 bean 依賴了十幾二十個 bean,再手動去配置,恐怕就很難受了。

 4.3 FactoryBean

FactoryBean?看起來是否是很像 BeanFactory 孿生兄弟。不錯,他們看起來很像,可是他們是不同的。FactoryBean 是一種工廠 bean,與普通的 bean 不同,FactoryBean 是一種能夠產生 bean 的 bean,好吧提及來很繞嘴。FactoryBean 是一個接口,咱們能夠實現這個接口。下面演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HelloFactoryBean implements FactoryBean<Hello> {

    @Override
    public Hello getObject() throws Exception {
        Hello hello = new Hello();
        hello.setContent("hello");
        return hello;
    }

    @Override
    public Class<?> getObjectType() {
        return Hello.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

配置以下:

1
<bean id="helloFactory" class="xyz.coolblog.service.HelloFactoryBean"/>

測試代碼以下:

1
2
3
4
5
6
7
8
9
10
public class ApplicationContextTest {

    @Test
    public void testFactoryBean() {
        String configLocation = "application-factory-bean.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
        System.out.println("helloFactory -> " + applicationContext.getBean("helloFactory"));
        System.out.println("&helloFactory -> " + applicationContext.getBean("&helloFactory"));
    }
}

測試結果以下:

由測試結果能夠看到,當咱們調用 getBean(「helloFactory」) 時,ApplicationContext 會返回一個 Hello 對象,該對象是 HelloFactoryBean 的 getObject 方法所建立的。若是咱們想獲取 HelloFactoryBean 自己,則能夠在 helloFactory 前加上一個前綴&,即&helloFactory

 4.4 factory-method

介紹完 FactoryBean,本節再來看看了一個和工廠相關的特性 – factory-method。factory-method 可用於標識靜態工廠的工廠方法(工廠方法是靜態的),直接舉例說明吧:

1
2
3
4
5
6
7
8
public class StaticHelloFactory {

    public static Hello getHello() {
        Hello hello = new Hello();
        hello.setContent("created by StaticHelloFactory");
        return hello;
    }
}

配置以下:

1
<bean id="staticHelloFactory" class="xyz.coolblog.service.StaticHelloFactory" factory-method="getHello"/>

測試代碼以下:

1
2
3
4
5
6
7
8
9
public class ApplicationContextTest {

    @Test
    public void testFactoryMethod() {
        String configLocation = "application-factory-method.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
        System.out.println("staticHelloFactory -> " + applicationContext.getBean("staticHelloFactory"));
    }
}

測試結果以下:

對於非靜態工廠,須要使用 factory-bean 和 factory-method 兩個屬性配合。關於 factory-bean 這裏就不繼續說了,留給你們本身去探索吧。

 4.5 lookup-method

lookup-method 特性可能你們用的很少(我也沒用過),不過它也是個有用的特性。在介紹這個特性前,先介紹一下背景。咱們經過 BeanFactory getBean 方法獲取 bean 實例時,對於 singleton 類型的 bean,BeanFactory 每次返回的都是同一個 bean。對於 prototype 類型的 bean,BeanFactory 則會返回一個新的 bean。如今考慮這樣一種狀況,一個 singleton 類型的 bean 中有一個 prototype 類型的成員變量。BeanFactory 在實例化 singleton 類型的 bean 時,會向其注入一個 prototype 類型的實例。可是 singleton 類型的 bean 只會實例化一次,那麼它內部的 prototype 類型的成員變量也就不會再被改變。但若是咱們每次從 singleton bean 中獲取這個 prototype 成員變量時,都想獲取一個新的對象。這個時候怎麼辦?舉個例子(該例子源於《Spring 揭祕》一書),咱們有一個新聞提供類(NewsProvider),這個類中有一個新聞類(News)成員變量。咱們每次調用 getNews 方法都想獲取一條新的新聞。這裏咱們有兩種方式實現這個需求,一種方式是讓 NewsProvider 類實現 ApplicationContextAware 接口(實現 BeanFactoryAware 接口也是能夠的),每次調用 NewsProvider 的 getNews 方法時,都從 ApplicationContext 中獲取一個新的 News 實例,返回給調用者。第二種方式就是這裏的 lookup-method 了,Spring 會在運行時對 NewsProvider 進行加強,使其 getNews 能夠每次都返回一個新的實例。說完了背景和解決方案,接下來就來寫點測試代碼驗證一下。

在演示兩種處理方式前,咱們先來看看不使用任何處理方式,BeanFactory 所返回的 bean 實例狀況。相關類定義以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class News {
    // 僅演示使用,News 類中無成員變量
}

public class NewsProvider {

    private News news;

    public News getNews() {
        return news;
    }

    public void setNews(News news) {
        this.news = news;
    }
}

配置信息以下:

1
2
3
4
<bean id="news" class="xyz.coolblog.lookupmethod.News" scope="prototype"/>
<bean id="newsProvider" class="xyz.coolblog.lookupmethod.NewsProvider">
    <property name="news" ref="news"/>
</bean>

測試代碼以下:

1
2
3
4
5
String configLocation = "application-lookup-method.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
NewsProvider newsProvider = (NewsProvider) applicationContext.getBean("newsProvider");
System.out.println(newsProvider.getNews());
System.out.println(newsProvider.getNews());

測試結果以下:

從測試結果中能夠看出,newsProvider.getNews() 方法兩次返回的結果都是同樣的,這個是不知足要求的。

 4.5.1 實現 ApplicationContextAware 接口

咱們讓 NewsProvider 實現 ApplicationContextAware 接口,實現代碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NewsProvider implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    private News news;

    /** 每次都從 applicationContext 中獲取一個新的 bean */
    public News getNews() {
        return applicationContext.getBean("news", News.class);
    }

    public void setNews(News news) {
        this.news = news;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

配置和測試代碼同上,測試結果以下:

這裏兩次獲取的 news 並就不是同一個 bean 了,知足了咱們的需求。

 4.5.2 使用 lookup-method 特性

使用 lookup-method 特性,配置文件須要改一下。以下:

1
2
3
4
<bean id="news" class="xyz.coolblog.lookupmethod.News" scope="prototype"/>
<bean id="newsProvider" class="xyz.coolblog.lookupmethod.NewsProvider">
    <lookup-method name="getNews" bean="news"/>
</bean>

NewsProvider 的代碼沿用 4.5.1 小節以前貼的代碼。測試代碼稍微變一下,以下:

1
2
3
4
5
6
String configLocation = "application-lookup-method.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
NewsProvider newsProvider = (NewsProvider) applicationContext.getBean("newsProvider");
System.out.println("newsProvider -> " + newsProvider);
System.out.println("news 1 -> " + newsProvider.getNews());
System.out.println("news 2 -> " + newsProvider.getNews());

測試結果以下:

從上面的結果能夠看出,new1 和 new2 指向了不一樣的對象。同時,你們注意看 newsProvider,彷佛變的很複雜。由此可看出,NewsProvider 被 CGLIB 加強了。

 4.6 depends-on

當一個 bean 直接依賴另外一個 bean,可使用 <ref/> 標籤進行配置。不過如某個 bean 並不直接依賴於其餘 bean,但又須要其餘 bean 先實例化好,這個時候就須要使用 depends-on 特性了。depends-on 特性比較簡單,就不演示了。僅貼一下配置文件的內容,以下:

這裏有兩個簡單的類,其中 Hello 須要 World 在其以前完成實例化。相關配置以下:

1
2
<bean id="hello" class="xyz.coolblog.depnedson.Hello" depends-on="world"/>
<bean id="world" class="xyz.coolblog.depnedson.World" />

 4.7 BeanPostProcessor

BeanPostProcessor 是 bean 實例化時的後置處理器,包含兩個方法,其源碼以下:

1
2
3
4
5
6
7
8
public interface BeanPostProcessor {
    // bean 初始化前的回調方法
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    // bean 初始化後的回調方法    
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}

BeanPostProcessor 是 Spring 框架的一個擴展點,經過實現 BeanPostProcessor 接口,咱們就可插手 bean 實例化的過程。好比你們熟悉的 AOP 就是在 bean 實例後期間將切面邏輯織入 bean 實例中的,AOP 也正是經過 BeanPostProcessor 和 IOC 容器創建起了聯繫。這裏我來演示一下 BeanPostProcessor 的使用方式,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 日誌後置處理器,將會在 bean 建立前、後打印日誌
 */
public class LoggerBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("Before " + beanName + " Initialization");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("After " + beanName + " Initialization");
        return bean;
    }
}

配置以下:

1
2
3
4
<bean class="xyz.coolblog.beanpostprocessor.LoggerBeanPostProcessor"/>
    
<bean id="hello" class="xyz.coolblog.service.Hello"/>
<bean id="world" class="xyz.coolblog.service.World"/>

測試代碼以下:

1
2
3
4
5
6
7
8
public class ApplicationContextTest {

    @Test
    public void testBeanPostProcessor() {
        String configLocation = "application-bean-post-processor.xml";
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(configLocation);
    }
}

測試結果以下:

與 BeanPostProcessor 相似的還有一個叫 BeanFactoryPostProcessor 拓展點,顧名思義,用戶能夠經過這個拓展點插手容器啓動的過程。不過這個不屬於本系列文章範疇,暫時先不細說了。

 4.8 BeanFactoryAware

Spring 中定義了一些列的 Aware 接口,好比這裏的 BeanFactoryAware,以及 BeanNameAware 和 BeanClassLoaderAware 等等。經過實現這些 Aware 接口,咱們能夠在運行時獲取一些配置信息或者其餘一些信息。好比實現 BeanNameAware 接口,咱們能夠獲取 bean 的配置名稱(beanName)。經過實現 BeanFactoryAware 接口,咱們能夠在運行時獲取 BeanFactory 實例。關於 Aware 類型接口的使用,能夠參考4.5.1 實現 ApplicationContextAware 接口一節中的敘述,這裏就不演示了。

 5. 閱讀源碼的一些建議

我在去年八月份的時候,嘗試過閱讀 Spring 源碼。不過 Spring 源碼太多了,調用複雜,看着看着就迷失在了代碼的海洋裏。不過好在當時找到了一個通過簡化後的類 Spring 框架,該框架黃億華前輩在學習 Spring 源碼時所寫的 tiny-spring。若是你們以爲看 Spring 源碼比較困難,能夠先學習一下 tiny-spring 的源碼,先對 Spring 代碼結構有個大概的瞭解。

另外也建議你們本身動手實現一個簡單的 IOC 容器,經過實踐,纔會有更多的感悟。我在去年八月份的時候,實現過一個簡單的 IOC 和 AOP(能夠參考我去年發的文章:仿照 Spring 實現簡單的 IOC 和 AOP - 上篇),並在最後將二者整合到了一塊兒。正是有了以前的實踐,才使得我對 Spring 的原理有了更進一步的認識。當作完一些準備工做後,再次閱讀 Spring 源碼,就沒之前那麼痛苦了。固然,Spring 的代碼通過十幾年的迭代,代碼量很大。我在分析的過程當中也只是儘可能保證搞懂重要的邏輯,沒法作到面面俱到。不過,若是你們願意去讀 Spring 源碼,我相信會比我理解的更透徹。

除了上面說的動手實踐外,在閱讀源碼的過程當中,若是實在看不懂,不妨調試一下。好比某個變量你不知道有什麼用,可是它又比較關鍵,在多個地方都出現了,顯然你必需要搞懂它。那麼此時寫點測試代碼調試一下,立馬就能知道它有什麼用了。

以上是我在閱讀源碼時所使用過的一些方法,固然僅有上面那些可能還不夠。本節的最後再推薦兩本書,以下:

第二本書在豆瓣上的評分不太好,不過我以爲還好。這本書我僅看了容器部分的代碼分析,總的來講還行。我在看循環依賴那一塊的代碼時,這本書仍是給了我一些幫助的。好了,本節就到這裏。

 6. 寫在最後

在本文的最後一章,我來講說我爲何閱讀 Spring 的源碼吧。對我我的而言,有兩個緣由。第一,做爲 Java Web 開發人員,咱們基本上繞不過 Spring 技術棧。固然除了 Spring,還有不少其餘的選擇(好比有些公司本身封裝框架)。但不能否認,Spring 如今還是主流。對於這樣一個常常打交道的框架,弄懂實現原理,還有頗有必要的。起碼在它出錯輸出一大堆異常時,你不會很心慌,能夠從容的 debug。第二,我去年的這個時候,工做差很少快滿一年。我在寫第一年的工做總結時,以爲很心慌。感受第一年好像只學到了一點開發的皮毛,技術方面,沒什麼積累。加上後來想換工做,內心想着就本身如今的水平恐怕要失業了。因此爲了平復本身焦慮的情緒,因而在去年的八月份的時候,我開始寫博客了。到如今寫了近30篇博客(比較少),其中有兩篇文章被博客平臺做爲優秀文章推薦過。如今,又快到7月份了,工齡立刻又要+1了。因此我如今在抓緊寫 Spring 相關的文章,但願在六月份多寫幾篇。算是對本身工做滿兩年的一個階段性技術總結,也是一個記念吧。

固然,看懂源碼並非什麼很了不得的事情,畢竟寫這些源碼的人才是真正的大神。我在大學的時候,自學 MFC(沒錯,就是微軟早已淘汰的東西)。並讀過侯捷老師著的《深刻淺出MFC》一書,這本書中的一句話對我影響至深 - 「勿在浮沙築高臺」。勿在浮沙築高臺,對於一個程序員來講,基礎很重要。因此我如今也在不斷的學習,但願能把基礎打好,這樣之後才能進入更高的層次。

好了,感謝你們耐心看完個人嘮叨。本文先到這裏,我要去寫後續的文章了,後面再見。bye~

 附錄:Spring 源碼分析文章列表

 Ⅰ. IOC

更新時間 標題
2018-05-30 Spring IOC 容器源碼分析系列文章導讀
2018-06-01 Spring IOC 容器源碼分析 - 獲取單例 bean
2018-06-04 Spring IOC 容器源碼分析 - 建立單例 bean 的過程
2018-06-06 Spring IOC 容器源碼分析 - 建立原始 bean 對象
2018-06-08 Spring IOC 容器源碼分析 - 循環依賴的解決辦法
2018-06-11 Spring IOC 容器源碼分析 - 填充屬性到 bean 原始對象
2018-06-11 Spring IOC 容器源碼分析 - 餘下的初始化工做

 Ⅱ. AOP

更新時間 標題
2018-06-17 Spring AOP 源碼分析系列文章導讀
2018-06-20 Spring AOP 源碼分析 - 篩選合適的通知器
2018-06-20 Spring AOP 源碼分析 - 建立代理對象
2018-06-22 Spring AOP 源碼分析 - 攔截器鏈的執行過程

 Ⅲ. MVC

更新時間 標題
2018-06-29 Spring MVC 原理探祕 - 一個請求的旅行過程
2018-06-30 Spring MVC 原理探祕 - 容器的建立過程
相關文章
相關標籤/搜索