天天看點

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

1. 簡介

Spring 是一個輕量級的企業級應用開發架構,于 2004 年由 Rod Johnson 釋出了 1.0 版本。經過十幾年的疊代,現在的 Spring 架構已經非常成熟了。Spring 包含了衆多子產品,包括但不限于 Core、Bean、Context、AOP 和 Web 等。在今天,我們完全可以使用 Spring 所提供的一站式解決方案開發出我們所需要的應用。作為 Java 程式員,我們會經常和 Spring 架構打交道,是以還是很有必要弄懂 Spring 的原理。

本文是 Spring IOC 容器源碼分析系列文章的第一篇文章,将會着重介紹 Spring 的一些使用方法和特性,為後續的源碼分析文章做鋪墊。另外需要特别說明一下,本系列的源碼分析文章是基于

Spring 4.3.17.RELEASE

版本編寫的,而非最新的

5.0.6.RELEASE

版本。好了,關于簡介先說到這裡,繼續來說說下面的内容。

 2. 文章編排

寫 Spring IOC 這一塊的文章,挺讓我糾結的。我原本是打算在一篇文章中分析所有的源碼,但是後來發現文章實在太長。主要是因為 Spring IOC 部分的源碼實在太長,将這一部分的源碼貼在一篇文章中還是很壯觀的。當然估計大家也沒興趣讀下去,是以決定對文章進行拆分。這裡先貼一張文章切分前的目錄結構:

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

如上圖,由目錄可以看出,假使在一篇文章中寫完所有内容,文章的長度将會非常長。是以在經過思考後,我會将文章拆分成一系列的文章,如下:

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

上面文章對應的源碼分析工作均已經完成,所有的文章将會在近期内進行更新。

 3. Spring 子產品結構

Spring 是分子產品開發的,Spring 包含了很多子產品,其中最為核心的是 bean 容器相關子產品。像 AOP、MVC、Data 等子產品都要依賴 bean 容器。這裡先看一下 Spring 架構的結構圖:

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

圖檔來源:Spring 官方文檔

從上圖中可以看出

Core Container

處于整個架構的最底層(忽略 Test 子產品),在其之上有 AOP、Data、Web 等子產品。既然 Spring 容器是最核心的部分,那麼大家如果要讀 Spring 的源碼,容器部分必須先弄懂。本篇文章作為 Spring IOC 容器的開篇文章,就來簡單介紹一下容器方面的知識。請繼續往下看。

 4. Spring IOC 部分特性介紹

本章将會介紹 IOC 中的部分特性,這些特性均會在後面的源碼分析中悉數到場。如果大家不是很熟悉這些特性,這裡可以看一下。

 4.1 alias

alias 的中文意思是“别名”,在 Spring 中,我們可以使用 alias 标簽給 bean 起個别名。比如下面的配置:

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"));
    }
}
           

測試結果如下:

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

 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"));
           

測試結果如下:

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

從測試結果可以看出,兩種方式配置方式都能完成解決 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"));
    }
}
           

測試結果如下:

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

由測試結果可以看到,當我們調用 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"));
    }
}
           

測試結果如下:

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

對于非靜态工廠,需要使用 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());
           

測試結果如下:

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

從測試結果中可以看出,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;
    }
}
           

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

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

這裡兩次擷取的 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());
           

測試結果如下:

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

從上面的結果可以看出,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);
    }
}
           

測試結果如下:

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

與 BeanPostProcessor 類似的還有一個叫 BeanFactoryPostProcessor 拓展點,顧名思義,使用者可以通過這個拓展點插手容器啟動的過程。不過這個不屬于本系列文章範疇,暫時先不細說了。

 4.8 BeanFactoryAware

Spring 中定義了一些列的 Aware 接口,比如這裡的 BeanFactoryAware,以及 BeanNameAware 和 BeanClassLoaderAware 等等。通過實作這些 Aware 接口,我們可以在運作時擷取一些配置資訊或者其他一些資訊。比如實作 BeanNameAware 接口,我們可以擷取 bean 的配置名稱(beanName)。通過實作 BeanFactoryAware 接口,我們可以在運作時擷取 BeanFactory 執行個體。關于 Aware 類型接口的使用,可以參考

4.5.1 實作 ApplicationContextAware 接口

一節中的叙述,這裡就不示範了。

轉載:http://www.tianxiaobo.com/2018/05/30/Spring-IOC-%E5%AE%B9%E5%99%A8%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0%E5%AF%BC%E8%AF%BB/