願你歸來時,仍是少年
Spring的模塊劃分參考上一篇:Spring源碼1 -Spring模塊總覽
有些書籍在開篇時,會讓我們去下載Spring的源碼,不過我覺得用maven直接創建項目,然後再引入需要的包,用maven下載源碼也挺方便的,這裏就不介紹怎麼去下整個源碼了,帶着一些問題從一個最簡單的例子開始。
1. 第一個例子
測試功能:先定義一個最簡單的javaBean,然後通過xml的方式進行配置,並在程序中讀取並使用該bean
簡單分析下,這裏只用到了Spring最基礎的功能,即spring-beans模塊,那麼,我們新建一個項目,引入spring-beans模塊即可。這裏爲了測試還引入了junit,maven項目的pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>springStudy</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.1.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
上面的<dependencyManagement> 依賴管理在上一節有提到過,spring爲了減少版本混亂引起的問題,提供了一個依賴管理的bom,定義了一個Spring版本的所有內部組件的版本,下面在<dependencies>中直接引用spring-beans模塊即可
上面的模塊引入之後,直接在idea中maven裏面把這個模塊的源碼下下來即可,後面調試的時候就可以看到帶註釋的源碼了
定義一個javaBean,下面看到這個bean只有一個field,和getter&setter
public class MyTestBean {
private String testStr = "testStr";
public String getTestStr() {
return testStr;
}
public void setTestStr(String testStr) {
this.testStr = testStr;
}
}
新建一個配置文件,引入這個bean
spring-beans.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">
<bean id="myTestBean" class="basic.MyTestBean"/>
</beans>
下面是測試類,測試一下定義的bean是否可以正常使用
package basic;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* Describe ***
* Created by zhanjp on 2018/6/7
*/
public class BeanFactoryTest {
@Test
public void testSimpleLoad(){
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("spring-beans.xml"));
MyTestBean bean = (MyTestBean) bf.getBean("myTestBean");
Assert.assertEquals("testStr", bean.getTestStr());
}
}
運行後,可以看到測試正常
可以看到程序的大體邏輯爲,告訴Spring要加載哪些beans,然後Spring根據配置生成了一個Bean的工廠,後面獲取Bean是通過這個工廠類得到的。
我們沒有去new一個實例,而是把控制權轉給了spring
下面看看Spring是怎麼把我們定義的配置文件,解析爲一個BeanFactory的。
2. 追蹤源碼
2.1 代碼1
new ClassPathResource("spring-beans.xml")
首先這句代碼,字面上理解就是加載一個類路徑下的資源文件,進入源碼
註釋說明了這裏創建了一個ClassPathResource,並使用一個thread context 類加載器去加載這個資源,即最後一行的ClassUtils.getDefaultClassLoader()
關於類加載器,可以看我的另外一篇博客https://mp.csdn.net/postedit/98119054
一般的類加載器,大部分採用雙親委派機制,而這裏使用了一個線程上下文類加載器,是一個破壞雙親委派機制的做法,因爲spring要讀取我們程序裏面的資源
到此爲止,我們可以看到ClassPathResource這個類做的事情還是比較簡單的,把我們傳的參數整理了一下,然後存到對象的屬性裏面,和生成了一個class context 的類加載器。
那麼ClassPathResource這個類出了hold住這兩個對象,還要做什麼功能呢?看下類結構圖
我們進入Resource接口中看一眼,它定義了所有Resource類提供的方法
內容不是很多,我們大概看方法名瞭解一下,然後繼續分析後面的代碼
2.2 代碼2
這裏初始化了一個XmlBeanFactory對象,先了解下XmlBeanFactory
類結構圖:
從類圖來看,這個類相當的複雜,頂層有不少的接口,BeanFactory、SingletenBeanRegistry、AliasRegistry,但是可以看到XmlBeanFactory直接繼承於DefaultListableBeanFactory,這個類對上面的接口做了大量的實現,只留了一小部分工作給XmlBeanFactory。
另外,可以看到XmlBeanFactory已經不被推薦使用了,類上面的註釋告訴我們,從Spring3.1開始,更推薦使用DefaultListableBeanFactory和XmlBeanDefinitionReader這兩個類。
由於上面的類過多且龐大,暫時不細緻的去說,先看看XmlBeanFactory幹了什麼。
這裏我們可以看到,XmlBeanFactory做的事情也很簡單,最關鍵的一行代碼,
this.reader.loadBeanDefinitions(resource);
通過一個Resource去加載Bean的定義。這個過程委託給了XmlBeanDefinitionReader這個類,而這個類的構造參數是this,說明這個Reader也依賴了當前的BeanFactory。
看到這裏有些疑問了,XmlBeanFactory和XmlBeanDefinitionReader互相依賴,Factory構造的時候需要reader屬性,而Reader在構建的時候又需要XmlBeanFactory。
實際上reader只是一個普通的屬性,而非靜態屬性,所以,當類初始化完成後,this已經存在,可以被用來生成XmlBeanDefinitionReader。如果reader屬性是個靜態屬性,就不能這麼玩了。可以看下面
下面進入XmlBeanDefinitionReader類看一看,還是先看結構圖
頂層有兩個接口,EnvironmentCapable和BeanDefinitionReader,前一個裏面只有一個方法,獲取當前組件關聯的Environment,主要看一下第二個接口
主要包括,獲取BeanDefinition註冊表、獲取資源加載器、獲取Bean的類加載器、獲取Bean的名稱生成器和加載Bean的定義的幾個方法。
而XmlBeanDefinitionReader這個類就比較複雜了,畢竟從XML裏面加載Bean的大部分代碼都在這裏,先跟到代碼裏看看
loadBeanDefinition方法沒什麼特殊的,就一個Resouce參數,轉成了EncodedResouce,這個類裏面主要是對特殊的編碼進行處理,而我們沒有做什麼特殊編碼的函數,可以直接先把當成一個Resouce來看。
直接看320行代碼,
resourcesCurrentlyBeingLoaded的初始化如下
NamedThreadLocal只是ThreadLocal的一個簡單的子類,多了一個name的屬性,大概是描述這個ThreadLocal變量是幹嘛用的。
可以看出,這個變量是爲了保存已經加載的resouce資源。
繼續往下看
325行,Spring嘗試把當前加載的資源文件放入已加載集合中,由於可能會有循環的import,碰到互相import的情況,這裏就會直接拋出異常了。可以看出這個集合的作用是防止循環引用變成死循環。
那下面可以看到最重要的地方了,
先從resouce中獲取輸入流,還記得 2.1 new ClassPathResouce的時候,還初始化了一個線程上下文類加載器嗎?進入getInputStream,可以看到
隱藏的還挺深的,這也是爲什麼看源碼這麼讓人頭疼的地方,畢竟不是自己寫的。。
好了,繼續,根據這個InputStream,332行初始化了一個InputSource,並設置了編碼。額,我們並沒有給什麼編碼,就認爲InputSource就是個InputStream吧。
下面就進入了336行
doLoadBeanDefinitions(inputSource, encodedResource.getResource());
開始加載bean的定義!
跟進代碼,這裏先是根據輸入流和Resouce初始化了一個Document類,這就是根據Xml解析出來的對象了。下面一行就是根據這個document解析並把Bean的定義註冊到Registry中了,並返回加載的數量。
這也是Spring加載Bean的最重要的兩個邏輯了
- Spring如何去解析Xml
- Spring如何去註冊Bean
3. Spring如何解析Xml?
首先進入代碼
Spring把加載XML資源的活全都委託給了documentLoader這個對象,這個對象的類型是DefaultDocumentLoader,實現了DocumentLoader接口。而DocumentLoader接口只有這一個實現類,可見這個DocumentLoader就是專門幹加載Xml資源這個活的。
接下來看看DefaultDocumentLoader類裏面的代碼
這裏的類結構大概可以看到,其實spring主要的解析Xml的方法,就是loadDocument這個方法,另外包含了兩個protected方法,和兩個靜態變量,
這兩個靜態變量分別用來配置如何驗證schema語言,和表示XSD schema語言的。從描述來看,是給JAXP驗證XML用的兩個屬性。
真正去解析xml的代碼,先創建了一個DocumentBuilderFactory然後再創建了一個DocumentBuilder,這兩個類都是javax.xml.parser包下面的,可見spring用來解析xml正式依賴了java系統的javax.xml.parser包,而這個包正是JAXP下面的,什麼是JAXP呢?就是Java api for xml processing,爲處理xml而制定的一套javaAPI。下面是JAXP的官方文檔:
https://docs.oracle.com/javase/8/docs/technotes/guides/xml/jaxp/
文檔的最下面還有一些jaxp提供的一些教程,這裏也貼一下
https://docs.oracle.com/javase/tutorial/jaxp/index.html
可以看到上面的代碼裏,需要的5個參數都是jaxp的參數,所以Spring解析XML而寫的DefaultDocumentLoader是爲了解析XML而專門包裝的工具類。
XML涉及的東西過多,這裏不再詳細介紹,有機會後面再寫一些使用JAXP的例子。相信有一些小例子更容易使我們理解Spring的解析XML的內容。
下一篇博客繼續