本文參考地址:
《spring Ioc/DI的理解》
《關於Spring IOC (DI-依賴注入)你需要知道的一切》
《一、IOC和DI的概念》
《深入理解IoC/DI》
《spring IOC篇二:xml的核心邏輯處理》
**溫馨提示:**前方內容會引起認真怪和女權者些許不適,請出門左手邊右拐。
一. 王大錘的相親市場
我叫王大錘,是個碼農,我們這個行業號稱“人傻錢多速來”,不信?呵呵呵呵呵呵呵……
我的職業是碼農,工作內容是 new 一個對象,日常聊天是如何找一個對象,睡覺是做夢如何 new 一個白富美對象陪我走上人生巔峯。總之,我沒有對象。
公司的同事連順看我工作繁忙無暇撩妹,同時又日漸飢渴難耐,最後還是建議我去婚介公司碰碰運氣,也許有個好運氣,或者找個盤接一下,再不濟也能遇見一羣飢渴男一起回家組隊打 Dota。於是我走到了春天婚介公司,踏上了登上人生巔峯之路。
大錘進入了春天婚介公司之後,主要辦了三件事:
- 進入春天婚介公司;
- 按照婚介公司要求,填寫個人用戶簡歷;
- 婚介公司告訴大錘,等待我們下一次的聯誼事宜:到時候會用很多本公司用戶參加,每個用戶都有自己的個人條件,屆時可進行配對或組隊;
大錘想了想還有點小激動,然後就回了公司,打開了自己的 Markdown,開始寫起了一篇控制反轉 (IoC) 和依賴注入 (DI) 相關的教程。
二. Spring IoC / DI 的簡單理解
在 《spring Ioc/DI的理解》 一文中,作者用人力資源局的例子方便讀者的理解。所以筆者按照自己對 IoC / DI 的理解,也編了一個王大錘婚介公司之旅的故事,用段子的方式寫出來,以期加深對自己和讀者的印象。
2.1 IoC 與 DI 的定義
首先,筆者需要明確說明 IoC 與 DI 的定義。
控制反轉 (Inversion of Control,即 IoC) 是一種設計思想。在 Java 開發中,IoC 意味着將你設計好的對象交給容器控制,而不是傳統的在你的對象內部直接控制。同時 IoC 也是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。
對於 IoC,它最常用的一種手段叫做依賴注入 (Dependency Injection,即 DI)。DI 通過控制反轉,對象在被創建的時候,由一個調控系統內所有對象的外界實體,將其所依賴的對象的引用傳遞給它。
上述定義也許比較難懂,所以筆者講了開頭的故事。故事中的春天婚介公司,就是我們經常使用的經典的 Spring IoC 容器。
將上面段子的名詞與 Spring 內容一一對應,則如下所示:
- 婚介公司 —— Spring IoC 容器
- 用戶徵婚簡歷 —— Spring beans
- 一次聯誼活動 —— xml 配置文件
- 月老 —— 開發者(我)
2.2 IoC 的三個經典問題
對於 IoC 有三個經典問題:**誰控制誰?控制了什麼?怎麼實現了反轉?**很多博客裏都進行了一個回答,筆者也按照自己的故事模式進行一個回答。
- “誰控制誰?”:IoC / DI 容器控制應用程序
- 其實可以把 IoC 當做一個存儲對象的容器,我們在開發中形成的對象都可以交給 Spring IoC 容器做一個統一的規範管理;
- 我們在開發中形成的對象可以用一個 Spring bean 來表示,所以可以聯想一下,Spring IoC 容器就是婚介公司,每個對象都向這個婚介公司投遞了徵婚簡歷,所有用戶簡歷全部由婚介公司調配。所以 IoC 容器就充當了一個婚姻介紹的角色;
- 控制了什麼?:IoC / DI 容器控制對象本身的創建、實例化,以及控制對象之間的依賴關係;
- 開發之中的對象已經全部交由 IoC 容器來管理了,那我們在獲取對象的時候,就得由 IoC 容器來給我們提供;
- 例:我們想要和一個妹子配對:
- 平常情況下,需要主動自己上去撩妹自己要(在實際工程中,即調用目標對象的 get API );
- 現在既然我和妹子都是婚介公司的用戶(都註冊了 bean),婚介公司控制了我們對於對象的獲取,那麼就得通過婚介公司來把妹子給你(在實際工程中,即調用 Spring 的 getBean 方法,中間的 BeanDefinition 等細節內容暫且不表);
- 怎麼實現了反轉?:主要體現在控制權的反轉。因爲現在應用程序不能主動去獲取外部資源了,而是被動等待 IoC / DI 容器給它注入它所需要的資源,所以稱之爲反轉。
- 例:依舊用上面的例子:我們想要和一個妹子配對:
- 平常情況下,我們直接去找妹子要過來,這種事情是我們自己去做的,控制權在我們手裏(實際工程中,就是在 classA 中需要一個 classB 的實例,所以就在 classA 中直接 new 了一個 classB 的實例來使用);
- 現在既然我和妹子都是婚介公司的用戶,那麼向婚介公司要求介紹這個妹子,讓婚介公司把妹子交給我們(實際工程中,就是通知 Spring IoC 容器“我需要 classB 的實例,你需要給我弄一個,然後把這個實例傳給 classA”);
- 這樣一對比,就發現創建權與控制權都從開發者身上轉移到了 Spring IoC 容器上,即實現了控制的反轉;
- 例:依舊用上面的例子:我們想要和一個妹子配對:
2.3 DI 的三個經典問題
同樣,DI 也存在三個經典問題:誰依賴誰?誰注入了誰?注入了什麼?
- “誰依賴誰?”:應用程序依賴於 IoC 容器;
- 上面也提到了我們找婚介公司介紹妹子配對的流程,可以看出我們用戶是依賴於婚介公司的,也就是應用程序依賴於 IoC 容器;
- “誰注入了誰?”:IoC 容器把對象注入於應用程序;
- 依舊是我們找婚介公司介紹妹子配對的流程,婚介公司把同爲用戶的妹子給了我們,就相當於 IoC 容器將對象注入到了應用程序之中;
- 這種我們需要了對象,IoC 將對象給我們的過程,就是依賴注入。
- “注入了什麼?”:注入應用程序需要的外部資源,比如有依賴關係的對象;
- 婚介公司把同爲用戶的妹子給了我們,就相當於 IoC 容器將對象注入到了應用程序之中;
此時,筆者可以通過一個類比,來把依賴注入的關係進行說明:
- 一個 xml 配置文件中,定義了若干 Spring beans;
- 即在一次聯誼活動中,會有很多用戶參加;
- 對於這些 Spring beans,就是定義 bean 時各種各樣的屬性定義;
- 對應於這些用戶,就是說每一個用戶都有自己的個人條件;
- 所謂個人條件,就是身長八尺,容貌甚偉,有房有車,Q大H好,醫卜星象門門會,鋼琴攝影樣樣通之類的;
- 根據開發者在 xml 配置文件中的定義,形成依賴關係。對於一個 bean 的依賴,可以依賴於一個 bean,也可以依賴於多個 bean;例如 bean 定義的 xml 配置文件中,會有類似於 p 命名空間的屬性注入 (p:name=“qixiaoxia”),或者是 ref 依賴關係注入 (pcbrand-ref=MacBookPro) 之類的配置關係,通過這些配置形成了依賴關係;
- 對於這次聯誼活動,月老牽線,一金童一玉女成功配對,喜結連理,從今以後過上沒羞沒臊的生活;當然也可能某個老司機勾搭上了多個用戶,形成了一個只屬於自己的小團伙,從此開啓了它的 S8 征戰之旅 (RNG IG 加油衝鴨!!!);
注:在《深入理解IoC/DI》中作者用一問一答的形式闡述了控制、依賴、注入等關係,以及與 IoC/DI 相關內容。本文有類似借鑑。
三. DI 的實現原理解析
控制反轉 IoC 與依賴注入 DI 之間的關係,控制反轉是目的,依賴注入是實現控制反轉的手段。前面也提到,如果在傳統模式中,A 類依賴於 B 類,就是在 A 類中 new 一個 B 類,或者是用 A 類的 set 方法將 B 類實例的引用注入 A 類。
但是 IoC 將生成類的方式把傳統模式反了過來,即開發人員不需要調用 new,而是在需要類的時候,由框架注入,由 DI 實現。即控制對象生成的權利,從自己轉移給了框架(即 Spring),或者比較淺顯的理解爲轉移給了 Spring 的 xml 配置文件。
筆者爲了模擬 DI 依賴注入的實現過程,按照文中的相親市場寫了一個簡單的 demo,用來測試依賴注入在源碼層面的實現。
測試源碼筆者已經上傳到筆者的 Github 上,地址:spring/DI
在 demo 中筆者設置了兩個類:
- 用戶 User
- 興趣愛好 Hobby
其中的 User 類中有三個屬性:
- String name: 姓名
- Hobby hobby: 興趣愛好
- User partner: 伴侶
每個用戶都有自己的姓名,一個愛好,還有配對完畢的伴侶。筆者的 bean xml 配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
xmlns:p="http://www.springframework.org/schema/p">
<bean id="qixiaoxia" class="com.grq.spring.DI.User"
p:name="qixiaoxia"
p:hobby-ref="qixiaoxiaHobby"
p:partner-ref="girlFriend"/>
<bean id="girlFriend" class="com.grq.spring.DI.User"
p:name="nsy"
p:partner-ref="qixiaoxia"/>
<bean id="qixiaoxiaHobby" class="com.grq.spring.DI.Hobby"
p:name="piano" p:level="Lv.8"/>
</beans>
Hobby 類定義源碼:
public class Hobby {
private String name;
private String level;
// get, set, 構造函數略
// ...
@Override
public String toString() {
return "Hobby{" +
"name='" + name + '\'' +
", level='" + level + '\'' +
'}';
}
}
User 類定義源碼:
public class User {
private String name;
private Hobby hobby;
private User partner;
// set, get, 構造函數略
// ...
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", hobby=" + hobby +
", partner=\'" + partner.getName() + "\'}";
}
}
測試用的 main 方法也很簡單:
public class DITest {
public static void main(String[] args) {
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("DITest.xml"));
User user = (User) factory.getBean("qixiaoxia");
System.out.println(user);
}
}
從 bean xml 配置文件解析內容的方法入口是 XmlBeanDefinitionReader # loadBeanDefinitions 方法,源碼及註釋如下:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
// 斷言要解析的XML文件配置存在,不能爲空
Assert.notNull(encodedResource, "EncodedResource must not be null");
// 向日志系統輸出日誌系統,輸出 XML bean 的加載源
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
// 獲取當前線程裏的 ThreadLocal 裏的變量集合
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
//如果爲空的情況下,重新申請一下 HashSet 集合
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
// 將 encodeResource 填加到當前線程的局部變量集合中
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
// 如果設置了編譯方式,對輸入流進行編碼的設置
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
//========================================
// 真正的從指定的 XML 文件中加載 Bean 的定義的關鍵方法
//========================================
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
// 釋放內存空間
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
該方法中,最關鍵的方法是 doLoadBeanDefinitions 方法,它真正的從指定的 XML 文件中加載了 Bean 的定義。源碼及註釋如下:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
// 獲取 XML 的驗證方式,加載 XML 文件得到對應的 Document
Document doc = doLoadDocument(inputSource, resource);
// 根據返回的 Dcoument 註冊 Bean 信息
return registerBeanDefinitions(doc, resource);
}
// 若干 catch 方法省略
// .........................................
}
doLoadBeanDefinitions 方法由兩個方法組成,一個是 doLoadDocument() 方法,其中獲取 XML 的驗證方式(如確定文件爲 DTD 或者 XSD 文件格式等相關信息),並將 xml 文檔信息放入 Document 實例對象中。該方法可在《spring IOC篇二:xml的核心邏輯處理》中查閱。
registerBeanDefinitions 是註冊 bean 的內容,其中依賴注入的過程就是在該部分進行的。源碼及註釋如下:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用 DefaultBeanDefinitionDocumentReader 實例化 BeanDefinitionDocumentReader 對象
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 記錄統計前 BeanDefinition 的加載個數
int countBefore = getRegistry().getBeanDefinitionCount();
// 加載以及註冊 Bean
// 這裏使用到了單一職責原則,將邏輯處理委託給單一的類進行處理,這個邏輯處理類就是 BeanDefinitionDocumentReader 對象
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 統計本次加載 Beanfinition 的個數
return getRegistry().getBeanDefinitionCount() - countBefore;
}
registerBeanDefinitions 是一個接口方法,它的具體實現是在 DefaultBeanDefinitionDocumentReader 中實現的:
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
this.logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
// 核心方法
this.doRegisterBeanDefinitions(root);
}
最後 doRegisterBeanDefinitions 方法纔是實際解析 xml 文件內容的核心方法。
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
//======================
// 處理 profile 屬性
//======================
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
}
// 空代碼留給子類去實現模板設計模式
// 繼承 DefaultBeanDefinitionDocumentReader 的子類在 XML 解析前做一些處理,可以實現此方法
preProcessXml(root);
//==============================
// 解析除了 profile 以外的默認屬性
//==============================
parseBeanDefinitions(root, this.delegate);
// 空代碼留給子類去實現模板設計模式
// 繼承 DefaultBeanDefinitionDocumentReader 的子類在 XML 解析後做一些處理,可以實現此方法
postProcessXml(root);
this.delegate = parent;
}
在 doRegisterBeanDefinitions 方法中,主要作用有三個:
- 處理了根節點 root 的 profile 屬性;
- 在該例程中,並沒有使用到 profile 屬性。
- 核心方法:調用 parseBeanDefinitions 方法,解析 bean 的基礎屬性。
- 在解析 bean 基礎屬性的上下文處進行預處理 preProcessXml, 後處理 postProcessXml,但兩個方法在該類中並沒有實際實現,而是採用了模板設計模式,留給繼承的子類,實現覆蓋該方法;
核心方法 parseBeanDefinitions 源碼如下:
// 從 XML 文件解析 Bean 的定義
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
// 獲取根節點的子節點列表
NodeList nl = root.getChildNodes();
// 遍歷子節點列表
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element)node;
if (delegate.isDefaultNamespace(ele)) {
// 解析當前節點
this.parseDefaultElement(ele, delegate);
} else {
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
// 解析 Bean 默認元素
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, "import")) {
this.importBeanDefinitionResource(ele);
} else if (delegate.nodeNameEquals(ele, "alias")) {
this.processAliasRegistration(ele);
} else if (delegate.nodeNameEquals(ele, "bean")) {
this.processBeanDefinition(ele, delegate);
} else if (delegate.nodeNameEquals(ele, "beans")) {
this.doRegisterBeanDefinitions(ele);
}
}
在覈心方法 parseBeanDefinitions 中,解析了 import, alias, bean, beans 四種標籤。我們的 bean xml 文件基本都是 <bean> 標籤,所以其中最核心的方法就是 processBeanDefinition 方法。processBeanDefinition 源碼如下:
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
// Bean 定義持有者
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
// 裝飾 Bean 定義,爲各個 bean 添加屬性信息,其中包含依賴關係的添加
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, this.getReaderContext().getRegistry());
} catch (BeanDefinitionStoreException var5) {
this.getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, var5);
}
this.getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
processBeanDefinition 方法源碼中,decorateBeanDefinitionIfRequired 方法將 xml 配置文件中各個 bean 的屬性“裝飾”到該 bean 的定義中,在該方法中實現了 bean 之間的依賴注入。源碼如下:
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder, BeanDefinition containingBd) {
BeanDefinitionHolder finalDefinition = definitionHolder;
// 獲取當前 bean 的所有屬性值
NamedNodeMap attributes = ele.getAttributes();
// 遍歷所有屬性值
for(int i = 0; i < attributes.getLength(); ++i) {
Node node = attributes.item(i);
//===============================================
// 關鍵方法,對當前屬性值進行裝飾(包含依賴關係注入的步驟)
//===============================================
finalDefinition = this.decorateIfRequired(node, finalDefinition, containingBd);
}
NodeList children = ele.getChildNodes();
for(int i = 0; i < children.getLength(); ++i) {
Node node = children.item(i);
if (node.getNodeType() == 1) {
finalDefinition = this.decorateIfRequired(node, finalDefinition, containingBd);
}
}
return finalDefinition;
}
在 decorateBeanDefinitionIfRequired 方法中遍歷部分的關鍵方法中,再向下可進入到裝飾屬性的內容實現方法 decorate。decorate 方法將一個 bean 在 xml 文件中的屬性定義賦值進入 BeanDefinition 中,該過程中當然也包含了 DI 依賴注入。decorate 方法是在 SimplePropertyNamespaceHandler 中實現的,源碼如下所示:
public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
if (node instanceof Attr) {
Attr attr = (Attr)node;
// 屬性名
String propertyName = parserContext.getDelegate().getLocalName(attr);
// 屬性值
String propertyValue = attr.getValue();
// 屬性值集合
MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues();
// 如果屬性值集合中已經包含了當前屬性名,則報出錯誤
if (pvs.contains(propertyName)) {
parserContext.getReaderContext().error("Property '" + propertyName + "' is already defined using both <property> and inline syntax. Only one approach may be used per property.", attr);
}
//===================================
// 依賴注入實現:
// 如果屬性名是以 "-ref" 結尾的,則將該屬性設置爲被依賴的 bean,即 RuntimeBeanReference
//===================================
if (propertyName.endsWith("-ref")) {
propertyName = propertyName.substring(0, propertyName.length() - "-ref".length());
// 將該屬性設置爲被依賴的 bean,即 RuntimeBeanReference,添加進入屬性值集合中
pvs.add(Conventions.attributeNameToPropertyName(propertyName), new RuntimeBeanReference(propertyValue));
} else {
pvs.add(Conventions.attributeNameToPropertyName(propertyName), propertyValue);
}
}
return definition;
}
注:關於 RuntimeBeanReference 的內容,可以在文章《Spring Bean 的解析 RuntimeBeanReference》一文中進行查閱瞭解。
對於例程中名爲 “qixiaoxia” 的 bean 進行調試,在 decorateBeanDefinitionIfRequired 方法中循環遍歷 decorate 方法之前的 beandefinition 變量的值如下圖所示:
經過 decorateBeanDefinitionIfRequired 方法循環賦值之前,如上圖所示,propertyValueList 爲空集。但在循環賦值後,結果如下圖所示:
propertyValueList 加入了三個值,這三個值與 xml 配置文件中 “qixiaoxia” bean 的定義相同,而且包含了其中以 “-ref” 爲屬性名的兩個屬性。可以對比 bean 的定義,以及上圖中循環後的 beandefinition 值的結果:
<bean id="qixiaoxia" class="com.grq.spring.DI.User"
p:name="qixiaoxia"
p:hobby-ref="qixiaoxiaHobby"
p:partner-ref="girlFriend"/>
這樣就將 bean 之間的依賴關係編輯完畢。往後將各個 beanDefinition 存入 Map 中並註冊,繼續運行 registerBeanDefinition 方法,即可完成 bean 從 xml 配置文件加載的操作。
至此,DI 依賴注入的源碼分析完畢。
四. 後記
第一次當標題黨,心裏還有點小激動呢 ~ 筆者用相親爲主題講了如何理解 IoC 和 DI,但標題黨不能白當,親還是要相的,萬一哪個有趣又美麗小姐姐看上我了呢?
筆者之自戀,有詩爲證:
鋼琴吉他 KTV,
攝影健身吹牛逼。
JAVA Python 還有 C,
有趣靈魂顏值帝。
最後獻上自拍一張,拜個晚年,各位中秋快樂 ~