一、老規矩,先比比點么蛾子
作為一個經常使用Spring的後端程式設計師,小編很早就想徹底弄懂整個Spring框架了!但它整體是非常大的,所有繼承圖非常複雜,加上小編修行尚淺,顯得力不從心。不過,男兒在世當立志,今天就先從Spring IOC容器的初始化開始說起,即使完成不了對整個Spring框架的完全掌握,也不丟人,因為小編動手了,穩住,我們能贏!
下面說一些閱讀前的建議:
- 1、閱讀原始碼分析是非常無聊的,但既然你進來了,肯定也是對這個東西進行了解,也希望這篇總結能對你有所啟發。
- 2、前方高能,文章可能會非常的長,圖文並茂。
- 3、閱讀前建議你對相關設計模式、軟體設計6大原則有所瞭解,小編會在行文中進行穿插。
- 4、小編在讀大四,學識尚淺,喜歡專研,如果你發現文章觀點有所錯誤或者與你見解有差異,歡迎評論區指出和交流!
- 5、建議你邊看文章的時候可以邊在IDE中進行除錯跟蹤
- 6、文章所有UML圖利用idea自動生成,具體生成方法為:
選中一個類名,先ctrl+shift+alt+U,再ctrl+alt+B,然後回車即可
二、文章將圍繞什麼來進行展開?
不多,就一行程式碼,如下圖:
這句是Spring初始化的程式碼,雖然只有一句程式碼,但內容賊多!
三、Spring 容器IOC 有哪些東西組成?
這樣子,小編先理清下思路,一步一步來:
- 1、上面那句程式碼有個檔案叫
applicationContext.xml
,這是個資原始檔,由於我們的bean
都在裡邊進行配置定義,那Spring總得對這個檔案進行讀取並解析
吧!所以Spring中有個模組叫Resource
模組,顧名思義,就是資源
嘛!用於對所有資源xml、txt、property
等檔案資源的抽象。關於對Resource
的更多知識,可以參考下邊兩篇文章:
下面先貼一張小編生成的類圖(圖片有點大,不知道會不會不清晰,如果不清晰可以按照上面說的idea生成方法去生成即可)
:
可以看到Resource
是整個體系的根介面,點進原始碼可以看到它定義了許多的策略方法
,因為它是用了策略模式
這種設計模式,運用的好處就是策略介面/類
定義了同一的策略,不同的子類有不同的具體策略實現,客戶端呼叫時傳入一個具體的實現物件比如UrlResource或者FileSystemResource
給策略介面/類Resource
即可!
所有策略
如下:
- 2、上面講了Spring框架對各種資源的抽象採用了
策略模式
,那麼問題來了,現在表示資源的東西有了,那麼是怎麼把該資源載入進來呢?於是就有了下面的ResourceLoader
元件,該元件負責對Spring資源的載入,資源指的是xml
、properties
等檔案資源,返回一個對應型別的Resource
物件。。UML圖如下:
從上面的UML圖可以看出,ResourceLoader
元件其實跟Resource
元件差不多,都是一個根介面,對應有不同的子類實現,比如載入來自檔案系統的資源,則可以使用FileSystemResourceLoader
,載入來自ServletContext
上下文的資源,則可以使用ServletContextResourceLoader
。
還有最重要的一點,從上圖看出,ApplicationContext
,AbstractApplication
是實現了ResourceLoader
的,這說明什麼呢?說明我們的應用上下文ApplicationContext
擁有載入資源的能力,這也說明了為什麼可以通過傳入一個String resource path
給ClassPathXmlApplicationContext("applicationContext.xml")
就能獲得xml檔案資源的原因了!清晰了嗎?nice!
- 3、上面兩點講到了,好!既然我們擁有了載入器
ResourceLoader
,也擁有了對資源的描述Resource
,但是我們在xml檔案中宣告的<bean/>
標籤在Spring又是怎麼表示的呢?注意這裡只是說對bean
的定義,而不是說如何將<bean/>
轉換為bean
物件。我想應該不難理解吧!就像你想表示一個學生Student
,那麼你在程式中肯定要宣告一個類Student
吧!至於學生資料是從excel
匯入,或者程式執行時new
出來,或者從xml
中載入進來這些都不重要,重要的是你要有一個將現實中的實體表示為程式中的物件的東西,所以<bean/>
也需要在Spring中做一個定義!於是就引入一個叫BeanDefinition
的元件,UML圖如下:
下面講解下UML圖:
首先配置檔案中的<bean/>
標籤跟我們的BeanDefinition
是一一對應的,<bean>
元素標籤擁有class
、scope
、lazy-init
等配置屬性,BeanDefinition
則提供了相應的beanClass
、scope
、lazyInit
屬性。
其中
RootBeanDefinition
是最常用的實現類,它對應一般性的<bean>
元素標籤,GenericBeanDefinition
是自2.5
以後新加入的bean
檔案配置屬性定義類,是一站式服務類。在配置檔案中可以定義父<bean>
和子<bean>
,父<bean>
用RootBeanDefinition
表示,而子<bean>
用ChildBeanDefiniton
表示,而沒有父<bean>
的<bean>
就使用RootBeanDefinition
表示。AbstractBeanDefinition
對兩者共同的類資訊進行抽象。Spring
通過BeanDefinition
將配置檔案中的<bean>
配置資訊轉換為容器的內部表示,並將這些BeanDefiniton
註冊到BeanDefinitonRegistry
中。Spring
容器的BeanDefinitionRegistry
就像是Spring
配置資訊的記憶體資料庫,主要是以map
的形式儲存,後續操作直接從BeanDefinitionRegistry
中讀取配置資訊。一般情況下,BeanDefinition
只在容器啟動時載入並解析,除非容器重新整理或重啟,這些資訊不會發生變化,當然如果使用者有特殊的需求,也可以通過程式設計的方式在執行期調整BeanDefinition
的定義。
- 4、有了載入器
ResourceLoader
,也擁有了對資源的描述Resource
,也有了對bean
的定義,我們不禁要問,我們的Resource
資源是怎麼轉成我們的BeanDefinition
的呢?因此就引入了BeanDefinitionReader
元件,Reader嘛!就是一種讀取機制,UML圖如下:
從上面可以看出,Spring 對reader進行了抽象,具體的功能交給其子類去實現,不同的實現對應不同的類,如PropertiedBeanDefinitionReader
,XmlBeanDefinitionReader
對應從Property和xml的Resource解析成BeanDefinition
。
其實這種讀取資料轉換成內部物件的,不僅僅是Spring專有的,比如:Dom4j解析器
SAXReader reader = new SAXReader(); Document doc = reader.read(url.getFile());
//url是一個URLResource物件 嚴格來說,都是Reader體系吧,就是將統一資源資料物件讀取轉換成相應內部物件。
- 5、好了!基本上所有元件都快齊全了!對了,還有一個元件,你有了
BeanDefinition
後,你還必須將它們註冊到工廠中去,所以當你使用getBean()
方法時工廠才知道返回什麼給你。還有一個問題,既然要儲存註冊這些bean
,那肯定要有個資料結構充當容器吧!沒錯,就是一個Map
,下面貼出BeanDefinitionRegistry
的一個實現,叫SimpleBeanDefinitionRegistry
的原始碼圖:
BeanDefinitionRegistry
的UML圖如下:
從圖中可以看出,BeanDefinitionRegistry
有三個預設實現,分別是SimpleBeanDefinitionRegistry
,DefaultListableBeanFactory
,GenericApplicationContext
,其中SimpleBeanDefinitionRegistry
,DefaultListableBeanFactory
都持有一個Map,也就是說這兩個實現類把儲存了bean。而GenericApplicationContext
則持有一個DefaultListableBeanFactory
物件引用用於獲取裡邊對應的Map。
在DefaultListableBeanFactory
中
在GenericApplicationContext
中
- 6、前面說的5個點基本上可以看出
ApplicationContext
上下文基本直接或間接貫穿所有的部分,因此我們一般稱之為容器
,除此之外,ApplicationContext
還擁有除了bean容器
這種角色外,還包括了獲取整個程式執行的環境引數等資訊(比如JDK版本,jre等),其實這部分Spring也做了對應的封裝,稱之為Enviroment
,下面就跟著小編的eclipse,一起debug下容器的初始化工程吧!
四、實踐是檢驗真理的唯一標準
學生類Student.java
如下:
package com.wokao666;
public class Student {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
public Student() {
super();
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
複製程式碼
在application.xml
中進行配置,兩個bean
:
<bean id="stu1" class="com.wokao666.Student">
<property name="id" value="1"></property>
<property name="name" value="xiaoming"></property>
<property name="age" value="21"></property>
</bean>
<bean id="stu2" class="com.wokao666.Student">
<property name="id" value="2"></property>
<property name="name" value="xiaowang"></property>
<property name="age" value="22"></property>
</bean>
複製程式碼
好了,接下來給最開頭那段程式碼打個斷點(Breakpoint
):
第一步:急切地載入ContextClosedEvent類,以避免在WebLogic 8.1中的應用程式關閉時出現奇怪的類載入器問題。
這一步無需太過在意!
第二步:既然是new ClassPathXmlApplicationContext()
那麼就呼叫構造器嘛!
第三步:
第四步:
好,我們跟著第三步中的super(parent)
,再結合上面第三節的第6小點UML圖一步一步跟蹤,然後我們來到AbstractApplicationContext
的這個方法:
那麼裡邊的resourcePatternResolver
的型別是什麼呢?屬於第三節說的6大步驟的哪個部分呢?通過跟蹤可以看到它的型別是ResourcePatternResolver
型別的,而ResourcePatternResolver
又是繼承了ResourceLoader
介面,因此屬於載入資源模組,如果還不清晰,我們們再看看ResourcePatternResolver
的原始碼即可,如下圖:
對吧!不僅繼承ResourceLoader
介面,而且只定義一個getResources()
方法用於返回Resource[]
資源集合。再者,這個介面還使用了策略模式
,其具體的實現都在實現類當中,好吧!來看看UML圖就知道了!
PathMatchingResourcePatternResolver
這個實現類呢!它就是用來解釋不同路徑資源的,比如你傳入的資源路徑有可能是一個常規的url
,又或者有可能是以classpath*
字首,都交給它處理。
ServletContextResourcePatternResolver
這個實現類顧名思義就是用來載入Servlet
上下文的,通常用在web中。
第五步:
接著第四步的方法,我們在未進入第四步的方法時,此時會對AbstractApplicationContext
進行例項化,此時this
物件的某些屬性被初始化了(如日誌物件)
,如下圖:
接著進入getResourcePatternResolver()
方法:
第四步說了,PathMatchingResourcePatternResolver
用來處理不同的資源路徑的,怎麼處理,我們先進去看看!
如果找到,此時控制檯會列印找到用於OSGi包URL解析的Equinox FileLocator
日誌。沒列印很明顯找不到!
執行完成返回setParent()
方法。
第六步:
如果父代是非null
,,則該父代與當前this
應用上下文環境合併。顯然這一步並沒有做什麼事!parent
顯然是null
的,那麼就不合並嘛!還是使用當前this
的環境。
做個總結:前六步基本上做了兩件事:
- 1、初始化相關上下文環境,也就是初始化
ClassPathXmlApplicationContext
例項 - 2、獲得一個
resourcePatternResolver
物件,方便第七步的資源解析成Resource
物件
第七步:
第七步又回到剛開始第三步的程式碼,因為我們前面6步已經完成對super(parent)
的追蹤。讓我們看看setConfigLocation()
方法是怎麼一回事~
/**
* Set the config locations for this application context.//未應用上下文設定資源路徑
* <p>If not set, the implementation may use a default as appropriate.//如果未設定,則實現可以根據需要使用預設值。
*/
public void setConfigLocations(String... locations) {
if (locations != null) {//非空
Assert.noNullElements(locations, "Config locations must not be null");//斷言保證locations的每個元素都不為null
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();//去空格,很好奇resolvePath做了什麼事情?
}
}
else {
this.configLocations = null;
}
}
複製程式碼
進入resolvePath()
方法看看:
/**
* 解析給定的資源路徑,必要時用相應的環境屬性值替換佔位符,應用於資源路徑配置。
* Resolve the given path, replacing placeholders with corresponding
* environment property values if necessary. Applied to config locations.
* @param path the original file path
* @return the resolved file path
* @see org.springframework.core.env.Environment#resolveRequiredPlaceholders(String)
*/
protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}
複製程式碼
進入getEnvironment()
看看:
/**
* {@inheritDoc}
* <p>If {@code null}, a new environment will be initialized via
* {@link #createEnvironment()}.
*/
@Override
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
this.environment = createEnvironment();
}
return this.environment;
}
複製程式碼
進入createEnvironment()
,方法,我們看到在這裡建立了一個新的StandardEnviroment
物件,它是Environment
的實現類,表示容器執行的環境,比如JDK環境,Servlet環境,Spring環境等等,每個環境都有自己的配置資料,如System.getProperties()
、System.getenv()
等可以拿到JDK環境資料;ServletContext.getInitParameter()
可以拿到Servlet環境配置資料等等,也就是說Spring抽象了一個Environment
來表示環境配置。
生成的StandardEnviroment
物件並沒有包含什麼內容,只是一個標準的環境,所有的屬性都是預設值。
總結:對傳入的path
進行路徑解析
第八步:這一步是重頭戲
先做個小結:到現在為止,我們擁有了以下例項:
現在程式碼執行到如下圖的refresh()
方法:
看一下這個方法的內容是什麼?
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 重新整理前準備工作,包括設定啟動時間,是否啟用標識位,初始化屬性源(property source)配置
prepareRefresh();
// 建立beanFactory(過程是根據xml為每個bean生成BeanDefinition並註冊到生成的beanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//準備建立好的beanFactory(給beanFactory設定ClassLoader,設定SpEL表示式解析器,設定型別轉化器【能將xml String型別轉成相應物件】,
//增加內建ApplicationContextAwareProcessor物件,忽略各種Aware物件,註冊各種內建的對賬物件【BeanFactory,ApplicationContext】等,
//註冊AOP相關的一些東西,註冊環境相關的一些bean
prepareBeanFactory(beanFactory);
try {
// 模板方法,為容器某些子類擴充套件功能所用(工廠後處理器)這裡可以參考BeanFactoryPostProcessor介面的postProcessBeanFactory方法
postProcessBeanFactory(beanFactory);
// 呼叫所有BeanFactoryPostProcessor註冊為Bean
invokeBeanFactoryPostProcessors(beanFactory);
// 註冊所有實現了BeanPostProcessor介面的Bean
registerBeanPostProcessors(beanFactory);
// 初始化MessageSource,和國際化相關
initMessageSource();
// 初始化容器事件傳播器
initApplicationEventMulticaster();
// 呼叫容器子類某些特殊Bean的初始化,模板方法
onRefresh();
// 為事件傳播器註冊監聽器
registerListeners();
// 初始化所有剩餘的bean(普通bean)
finishBeanFactoryInitialization(beanFactory);
// 初始化容器的生命週期事件處理器,併發布容器的生命週期事件
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// 銷燬已建立的bean
destroyBeans();
// 重置`active`標誌
cancelRefresh(ex);
throw ex;
}
finally {
//重置一些快取
resetCommonCaches();
}
}
}
複製程式碼
在這裡我想說一下,這個refresh()
方法其實是一個模板方法,很多方法都讓不同的實現類去實現,但該類本身也實現了其中一些方法,並且這些已經實現的方法是不允許子類重寫的,比如:prepareRefresh()
方法。更多模板方法設計模式,可看我之前的文章 談一談我對‘模板方法’設計模式的理解(Template)
先進入prepareRefresh()
方法:
/**
* Prepare this context for refreshing, setting its startup date and
* active flag as well as performing any initialization of property sources.
*/
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();//設定容器啟動時間
this.closed.set(false);//容器關閉標誌,是否關閉?
this.active.set(true);//容器啟用標誌,是否啟用?
if (logger.isInfoEnabled()) {//執行到這裡,控制檯就會列印當前容器的資訊
logger.info("Refreshing " + this);
}
// 空方法,由子類覆蓋實現,初始化容器上下文中的property檔案
initPropertySources();
//驗證標記為必需的所有屬性均可解析,請參閱ConfigurablePropertyResolver#setRequiredProperties
getEnvironment().validateRequiredProperties();
//允許收集早期的ApplicationEvents,一旦多播器可用,即可釋出...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}
複製程式碼
控制檯輸出:
三月 22, 2018 4:21:13 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
資訊: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@96532d6: startup date [Thu Mar 22 16:21:09 CST 2018]; root of context hierarchy
複製程式碼
第九步:
進入obtainFreshBeanFactory()
方法:
/**
* 告訴子類重新整理內部bean工廠(子類是指AbstractApplicationContext的子類,我們使用的是ClassPathXmlApplicationContext)
* Tell the subclass to refresh the internal bean factory.
*/
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();//重新整理Bean工廠,如果已經存在Bean工廠,那就關閉並銷燬,再建立一個新的bean工廠
ConfigurableListableBeanFactory beanFactory = getBeanFactory();//獲取新建立的Bean工廠
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);//控制檯列印
}
return beanFactory;
}
複製程式碼
進入refreshBeanFactory()
方法:
/**
* 該實現執行該上下文的基礎Bean工廠的實際重新整理,關閉以前的Bean工廠(如果有的話)以及為該上下文的生命週期的下一階段初始化新鮮的Bean工廠。
* This implementation performs an actual refresh of this context's underlying
* bean factory, shutting down the previous bean factory (if any) and
* initializing a fresh bean factory for the next phase of the context's lifecycle.
*/
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {//如果已有bean工廠
destroyBeans();//銷燬
closeBeanFactory();//關閉
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();//建立一個新的bean工廠
beanFactory.setSerializationId(getId());//為序列化目的指定一個id,如果需要,可以將此BeanFactory從此id反序列化回BeanFactory物件。
//定製容器,設定啟動引數(bean可覆蓋、迴圈引用),開啟註解自動裝配
customizeBeanFactory(beanFactory);
////將所有BeanDefinition載入beanFactory中,此處依舊是模板方法,具體由子類實現
loadBeanDefinitions(beanFactory);
//beanFactory同步賦值
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
複製程式碼
總結:這一步主要的工作就是判斷重新整理容器前是否已經有beanfactory存在,如果有,那麼就銷燬舊的beanfactory,那麼就銷燬掉並且建立一個新的beanfactory返回給容器,同時將xml檔案的BeanDefinition
註冊到beanfactory中。如果不太清楚可以回過頭看看我們的第三節第5點內容
第十步:
進入第九步的loadBeanDefinitions(beanFactory)
方法中去take a look
:
/**
* 使用XmlBeanDefinitionReader來載入beandefnition,之前說過使用reader機制載入Resource資源變為BeanDefinition物件
* Loads the bean definitions via an XmlBeanDefinitionReader.
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* @see #initBeanDefinitionReader
* @see #loadBeanDefinitions
*/
@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 建立XmlBeanDefinitionReader物件
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// 使用當前上下文Enviroment中的Resource配置beanDefinitionReader,因為beanDefinitionReader要將Resource解析成BeanDefinition嘛!
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
//初始化這個reader
initBeanDefinitionReader(beanDefinitionReader);
//將beandefinition註冊到工廠中(這一步就是將bean儲存到Map中)
loadBeanDefinitions(beanDefinitionReader);
}
複製程式碼
控制檯輸出:
三月 22, 2018 5:09:40 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
資訊: Loading XML bean definitions from class path resource [applicationContext.xml]
複製程式碼
第十一步:
進入prepareBeanFactory(beanFactory)
方法:
//設定bean類載入器
//設定Spring語言表示式(SpEL)解析器
//掃描ApplicationContextAware bean
//註冊類載入期型別切面織入(AOP)LoadTimeWeaver
//為各種載入進入beanFactory的bean配置預設環境
複製程式碼
第十二步:
postProcessBeanFactory(beanFactory)
方法:
postProcessBeanFactory
同樣作為一個模板方法,由子類來提供具體的實現,子類可以有自己的特殊對BeanDefinition
後處理方法,即子類可以在這對前面生成的BeanDefinition
,即bean
的後設資料再處理。比如修改某個bean
的id/name
屬性、scope
屬性、lazy-init
屬性等。
第十三步:
invokeBeanFactoryPostProcessors(beanFactory)
方法:
該方法呼叫所有的BeanFactoryPostProcessor
,它是一個介面,實現了此介面的類需重寫postProcessBeanFactory()
這個方法,可以看出該方法跟第十二步的方法是一樣的,只不過作為介面,更多的是提供給開發者來對生成的BeanDefinition
做處理,由開發者提供處理邏輯。
第十四步:
其餘剩下的方法基本都是像初始化訊息處理源
,初始化容器事件
,註冊bean監聽器到事件傳播器上
,最後完成容器重新整理。
五、總結
恭喜我,我終於寫完了,同樣也恭喜你,你也閱讀完了。
我很佩服我自己能花這麼長時間進行總結髮布,之所以要進行總結,那是因為小編還是贊同好記性不如爛筆頭
的說法。
你不記,你過陣子就會忘記,你若記錄,你過陣子也會忘記!區別在於忘記了,可以回過頭在很短的時間內進行回憶,查漏補缺,減少學習成本。
再者,我認為我分析的還不是完美的,缺陷很多,因此我將我寫的所有文章釋出出來和大家探討交流,汕頭大學有校訓說得非常地好,那就是說之知識是用來共享的,因為共享了,知識才能承前啟後。
現在再梳理一下Spring初始化過程:
- 1、首先初始化上下文,生成
ClassPathXmlApplicationContext
物件,在獲取resourcePatternResolver
物件將xml
解析成Resource
物件。 - 2、利用1生成的context、resource初始化工廠,並將resource解析成beandefinition,再將beandefinition註冊到beanfactory中。
朋友們,發現毛病,請評論告訴小編,一起交流一起交流!