一、前言
作為一個經常使用 Spring 的后端程式員,很早就想徹底弄懂整個 Spring 框架了!但它整體是非常大的.
spring ioc 思維導圖:點擊領取spring全套視頻與原始碼:暗號CSDN
最新2020整理收集的一線互聯網公司面試真題(都整理成檔案),有很多干貨,包含netty,spring,執行緒,spring cloud等詳細講解,也有詳細的學習規劃圖,面試題整理等,我感覺在面試這塊講的非常清楚:獲取面試資料只需:點擊這里領取!!! 暗號:CSDN
二、文章將圍繞什么來進行展開?
不多,就一行代碼,如下圖:

這句是 Spring 初始化的代碼,雖然只有一句代碼,但內容賊多!
三、Spring 容器 IOC 有哪些東西組成?
這樣子,小編先理清下思路,一步一步來:
1、上面那句代碼有個檔案叫applicationContext.xml, 這是個資源檔案,由于我們的bean都在里邊進行配置定義,那 Spring 總得對這個檔案進行讀取并決議吧!所以 Spring 中有個模塊叫Resource模塊,顧名思義,就是資源嘛!用于對所有資源xml、txt、property等檔案資源的抽象,
下面先貼一張小編生成的類圖(圖片有點大,不知道會不會不清晰,如果不清晰可以按照上面說的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 檔案中宣告的標簽在 Spring 又是怎么表示的呢?注意這里只是說對bean的定義,而不是說如何將轉換為bean物件,我想應該不難理解吧!就像你想表示一個學生Student,那么你在程式中肯定要宣告一個類Student吧!至于學生資料是從excel匯入,或者程式運行時new出來,或者從xml中加載進來這些都不重要,重要的是你要有一個將現實中的物體表示為程式中的物件的東西,所以也需要在 Spring 中做一個定義!于是就引入一個叫BeanDefinition的組件,UML 圖如下:
下面講解下 UML 圖:
首先組態檔中的標簽跟我們的BeanDefinition是一一對應的,元素標簽擁有class、scope、lazy-init等配置屬性,BeanDefinition則提供了相應的beanClass、scope、lazyInit屬性,
其中RootBeanDefinition是最常用的實作類,它對應一般性的元素標簽,GenericBeanDefinition是自2.5以后新加入的bean檔案配置屬性定義類,是一站式服務類,在組態檔中可以定義父和子,父用RootBeanDefinition表示,而子用ChildBeanDefiniton表示,而沒有父的就使用RootBeanDefinition表示,AbstractBeanDefinition對兩者共同的類資訊進行抽象, Spring通過BeanDefinition將組態檔中的配置資訊轉換為容器的內部表示,并將這些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有三個默認實作,分別是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 + ", ]";
}
}
在application.xml中進行配置,兩個bean:
<bean id="stu1" class="com.wokao666.Student">
<property ></property>
<property ></property>
<property ></property>
</bean>
<bean id="stu2" class="com.wokao666.Student">
<property ></property>
<property ></property>
<property ></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 中,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/204154.html
標籤:python
