造輪子:實現一個簡易的 Spring IoC 容器

DeppWXQ發表於2020-04-19

作者:DeppWang原文地址

source:https://fernandofranzini.wordpress.com/

我通過實現一個簡易的 Spring IoC 容器,算是入門了 Spring 框架。本文是對實現過程的一個總結提煉,需要配合原始碼閱讀原始碼地址

結合本文和原始碼,你應該可以學到:Spring 的原理和 Spring Boot 的原理。

Spring 框架是 Java 開發的,Java 是物件導向的語言,所以 Spring 框架本身有大量的抽象、繼承、多型。對於初學者來說,光是理清他們的邏輯就很麻煩,我摒棄了那些包裝,只實現了最本質的功能。程式碼不是很嚴謹,但只為了理解 Spring 思想卻夠了。

下面正文開始。

零、前言

在沒有 Spring 框架的遠古時代,我們業務邏輯長這樣:

public class PetStoreService {
    AccountDao accountDao = new AccountDao();
}

public class AccountDao {
}

PetStoreService petStoreService = new PetStoreService();

到處都是 new 關鍵字,需要開發人員顯式的使用 new 關鍵字來建立業務類物件(例項)。這樣有很多弊端,如,建立的物件太多,耦合性太強,等等。

有個叫 Rod Johnson 老哥對此很不爽,就開發了一個叫 Spring 的框架,就是為了幹掉 new 關鍵字(哈哈,我杜撰的,只是為了說明 Spring 的作用)。

有了 Spring 框架,由框架來新建物件,管理物件,並處理物件之間的依賴,我們程式設計師再也不用 new 業務類物件了。我們來看看 Spring 框架是如何實現的吧。

注:以下 Spring 框架簡寫為 Spring

本節原始碼對應:v0

一、實現「例項化 Bean 」

首先,Spring 需要例項化類,將其轉換為物件。在 Spring 中,我們管(業務)類叫 Bean,所以例項化類也可以稱為例項化 Bean。

早期 Spring 需要藉助 xml 配置檔案來實現例項化 Bean,可以分為三步(配合原始碼 v1 閱讀):

  1. 從 xml 配置檔案獲取 Bean 資訊,如全限定名等,將其作為 BeanDefinition(Bean 定義類)的屬性
  2. 使用一個 Map 存放所有 BeanDefinition,此時 Spring 本質上是一個 Map,存放 BeanDefinition
  3. 當獲取 Bean 例項時,通過類載入器,根據全限定名,得到其類物件,通過類物件利用反射建立 Bean 例項

關於類載入和反射,前者可以看看《深入理解 Java 虛擬機器》第 7 章,後者可以看看《廖雪峰 Java 教程》反射 部分。本文只學習 Spring,這兩個知識點不做深入討論。

名詞解釋:

  • 全限定名:指編譯後的 class 檔案在 jar 包中的路徑

本節原始碼對應:v1

二、實現「填充屬性(依賴注入)」

實現例項化 Bean 後,此時成員變數(引用)還為 null:

此時需要通過一種方式實現,讓引用指向例項,我們管這一步叫填充屬性。

當一個 Bean 的成員變數型別是另一個 Bean 時,我們可以說一個 Bean 依賴於另一個 Bean。所以填充屬性,也可以稱為依賴注入(Dependency Injection,簡稱 DI)。

拋開 Spring 不談,在正常情況下,我們有兩種方式實現依賴注入,1、使用 Setter() 方法,2、使用構造方法。使用 Setter() 方法如下:

public class PetStoreService {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
}

public class AccountDao {
}

PetStoreService petStore = new PetStoreService();
petStore.setAccountDao(new AccountDao()); // 將依賴 new AccountDao() 注入 petStore

其實早期 Spring 也是通過這兩種方式來實現依賴注入的。下面是 Spring 通過 xml 檔案 + Setter() 來實現依賴注入的步驟(配合原始碼 v2 閱讀):

  1. 給 PetStoreService 新增 Setter() 方法,並稍微修改一下 xml 配置檔案,新增 <property>,代表對應 Setter() 方法。
  2. 從 xml 配置檔案獲取 Bean 的屬性 <property>,存放到 BeanDefinition 的 propertyNames 中。
  3. 通過 propertyName 獲取屬性例項,利用反射,通過 Setter() 方法實現填充屬性(依賴注入)

基於建構函式實現依賴注入的方式跟 Setter() 方法差不多,感興趣可以 Google 搜尋檢視。

因為 Spring 實現了依賴注入,所以我們程式設計師沒有了建立物件的控制權,所以也被稱為控制反轉(Inversion of Control,簡稱 IoC)。因為 Spring 使用 Map 管理 BeanDefinition,我們也可以將 Spring 稱為 IoC 容器。

本節原始碼對應:v2

三、使用「單例模式、工廠方法模式」

前面兩步實現了獲取 Bean 例項時建立 Bean 例項,但 Bean 例項經常使用,不能每次都新建立。其實在 Spring 中,一個業務類只對應一個 Bean 例項,這需要使用單例模式。

單例模式:一個類有且只有一個例項

Spring 使用類物件建立 Bean 例項,是如何實現單例模式的?

Spring 其實使用一個 Map 存放所有 Bean 例項。建立時,先看 Map 中是否有 Bean 例項,沒有就建立;獲取時,直接從 Map 中獲取。這種方式能保證一個類只有一個 Bean 例項。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(64);

早期 Spring 使用 Bean 的策略是用到時再例項化所用 Bean,傑出代表是 XmlBeanFactory,後期為了實現更多的功能,新增了 ApplicationContext,兩者都繼承於 BeanFactory 介面。這使用了工廠方法模式。

工廠方法模式:定義一個用於建立物件的介面,讓子類決定例項化哪一個類。Factory Method 使一個類的例項化延遲到其子類。

我們將 BeanIocContainer 修改為 BeanFactory 介面,只提供 getBean() 方法。建立(IoC)容器由其子類自己實現。

ApplicationContext 和 BeanFactory 的區別:ApplicationContext 初始化時就例項化所有 Bean,BeanFactory 用到時再例項化所用 Bean。

本節原始碼對應:v3

三、實現「註解」

前面使用 xml 配置檔案的方式,實現了例項化 Bean 和依賴注入。這種方式比較麻煩,還容易出錯。Spring 從 2.5ref 開始可使用註解替代 xml 配置檔案。比如:

  1. 使用 @Component 註解代替 <bean>
  2. 使用 @Autowired 註解代替 <property>

@Component 用於生成 BeanDefinition,原理(配合原始碼 v4 閱讀):

  • 根據 component-scan 指定路徑,找到路徑下所有包含 @Component 註解的 Class 檔案,作為 BeanDefinition
  • 如何判斷 Class 是否有 @Component:利用位元組碼技術,獲取 Class 檔案中的後設資料(註解等),判斷後設資料中是否有 @Componet

@Autowired 用於依賴注入,原理(配合原始碼 v4 閱讀):

  • 通過反射,檢視 Field 中是否有 @Autowired 型別的註解,有,則使用反射實現依賴注入

至此,我們還是在需要通過配置檔案來實現元件掃描。有沒有完全不使用配置檔案的方式?有!

我們可以使用 @Configuration 替代配置檔案,並使用 @ComponentScan 來替代配置檔案的 <context:component-scan>

@Configuration // 將類標記為 @Configuration,代表這個類是相當於一個配置檔案
@ComponentScan // ComponentScan 掃描 PetStoreConfig.class 所在路徑及其所在路徑所有子路徑的檔案
public class PetStoreConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PetStoreConfig.class);
        PetStoreService userService = context.getBean(PetStoreService.class);
        userService.getAccountDao();
    }
}

使用註解其實跟使用 xml 配置檔案一樣,目的是將配置類作為入口,實現掃描元件,將其載入進 IoC 容器中的功能。

AnnotationConfigApplicationContext 是專為針對配置類的啟動類。其實現機制,可以 Google 查閱。

名詞解釋:

  • Component:元件
  • Autowired:自動裝配

本節原始碼對應:v4

四、Spring Boot 原理

說到了 @Configuration 和 @ComponentScan,就不得不提 Spring Boot。因為 Spring Boot 就使用了 @Configuration 和 @ComponentScan,你可以點開 @SpringBootApplication 看到。

我們發現,Spring Boot 啟動時,並沒有使用 AnnotationConfigApplicationContext 來指定啟動某某 Config 類。這是因為它使用了 @EnableAutoConfiguration 註解。

Spring Boot 利用了 @EnableAutoConfiguration 來自動載入標識為 @Configuration 的配置類到容器中。Spring Boot 還可以將需要自動載入的配置類放在 spring.factories 中,Spring Boot 將自動載入 spring.factories 中的配置類。spring.factories 需放置於META-INF 下。

如 Spring Boot 專案啟動時,autocofigure 包中將自動載入到容器的(部分)配置類如下:

以上也是 Spring Boot 的原理。

在 Spring Boot 中,我們引入的 jar 包都有一個欄位,starter,我們叫 starter 包。

標識為 starter(啟動器)是因為引入這些包時,我們不用設定額外操作,它能被自動裝配,starter 包一般都包含自己的 spring.factories。如 spring-cloud-starter-eureka-server:

如 druid-spring-boot-starter:

有時候我們還需要自定義 starter 包,比如在 Spring Cloud 中,當某個應用要呼叫另一個應用的程式碼時,要麼呼叫方使用 Feign(HTTP),要麼將被呼叫方自定義為 starter 包,讓呼叫方依賴引用,再 @Autowired 使用。此時需要在被呼叫方設定配置類和 spring.factories:

@Configuration
@ComponentScan
public class ProviderAppConfiguration {
}

// spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.amy.cloud.amycloudact.ProviderAppConfiguration

當然,你也可以把這兩個檔案放在呼叫方(此時要指定掃描路徑),但一般放在被呼叫方。ps:如果你兩個應用的 base-package 路徑一樣,那麼可以不用這一步。

說了 Spring Boot,那麼在 Spring MVC,如何將引入 jar 包的元件注入容器?

  • 跟掃描本專案包一樣,在 xml ,增加引入 jar 包的掃描路徑:
<context:component-scan base-package="引入 jar 包的 base-package" />
...

嗯,本節沒有原始碼

五、結語

以上實現了一個簡易的 Spring IoC 容器,順便說了一下 Spring Boot 原理。Spring 還有很多重要功能,如:管理 Bean 生命週期、AOP 的實現,等等。後續有機會再做一次分享。

來個註解小結:

  • Spring 只例項化標識為 @Component 的元件(即業務類物件)
  • @Component 作為元件標識
  • @Autowired 用於判斷是否需要依賴注入
  • @ComponentScan 指定元件掃描路徑,不指定即為當前路徑
  • @Configuration 代表配置類,作為入口
  • @EnableAutoConfiguration 實現載入配置類

有的童鞋可能還會有這樣的疑問:

jdk jar 包、工具 jar 包的類是否需要注入容器?

  • 回答是不需要,因為容器只管理業務類,注入容器的類都有 @Component 註解。

全文完。

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章