作者: "DeppWang" 、 "原文地址" 我通過實現一個簡易的 Spring IoC 容器,算是入門了 Spring 框架。本文是對實現過程的一個總結提煉, 需要配合源碼閱讀 , "源碼地址" 。 結合本文和源碼,你應該可以學到:Spring 的原理和 Spring Boot 的原理。 Spr ...
我通過實現一個簡易的 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 閱讀):
- 從 xml 配置文件獲取 Bean 信息,如全限定名等,將其作為 BeanDefinition(Bean 定義類)的屬性
- 使用一個 Map 存放所有 BeanDefinition,此時 Spring 本質上是一個 Map,存放 BeanDefinition
- 當獲取 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 閱讀):
- 給 PetStoreService 添加 Setter() 方法,並稍微修改一下 xml 配置文件,添加
<property>
,代表對應 Setter() 方法。 - 從 xml 配置文件獲取 Bean 的屬性
<property>
,存放到 BeanDefinition 的 propertyNames 中。 - 通過 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 配置文件。比如:
- 使用 @Component 註解代替
<bean>
- 使用 @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 發佈!