1. 程式人生 > >Spring框架講解,Spring Boot 學習指南

Spring框架講解,Spring Boot 學習指南

 

在過去兩三年的 Spring 生態圈,最讓人興奮的莫過於 Spring Boot 框架。Spring Boot 應用本質上就是一個基於 Spring 框架的應用,它是 Spring 對“約定優先於配置”理念的最佳實踐產物,它能夠幫助開發者更快速高效地構建基於 Spring 生態圈的應用。

 

那 Spring Boot 有何魔法?自動配置、起步依賴、Actuator、命令列介面(CLI) 是Spring Boot 最重要的 4 大核心特性,本文將為你開啟 Spring Boot 的大門,重點為你剖析其啟動流程以及自動配置實現原理。

 

一、拋磚引玉:探索 Spring IoC 容器

 

如果有看過 SpringApplication.run()方法的原始碼,Spring Boot 冗長無比的啟動流程一定會讓你抓狂,透過現象看本質,SpringApplication 只是將一個典型的Spring 應用的啟動流程進行了擴充套件,因此,透徹理解 Spring 容器是開啟 Spring Boot 大門的一把鑰匙。

 

1.1、Spring IoC 容器

可以把 Spring IoC 容器比作一間餐館,當你來到餐館,通常會直接招呼服務員:點菜!可能你根本不關心菜的原料是什麼。IoC 容器也是一樣,你只需要告訴它需要某個 bean,它就把對應的例項(instance)扔給你,至於這個 bean 是否依賴其他元件,怎樣完成它的初始化,根本就不需要你關心。

 

作為餐館,想要做出菜餚,得知道菜的原料和菜譜,同樣地,IoC 容器想要管理各個業務物件以及它們之間的依賴關係,需要通過某種途徑來記錄和管理這些資訊。 BeanDefinition 物件就承擔了這個責任:

 

容器中的每一個 bean 都會有一個對應的 BeanDefinition 例項,該例項負責儲存 bean 物件的所有必要資訊,包括 bean 物件的 class 型別、是否是抽象類、構造方法和引數、其它屬性等等。當客戶端向容器請求相應物件時,容器就會通過這些資訊為客戶端返回一個完整可用的 bean 例項。

 

原材料已經準備好(把 BeanDefinition 看作原料),你還需要一份菜譜,BeanDefinitionRegistry 和 BeanFactory 就是這份菜譜,BeanDefinitionRegistry 抽象出 bean 的註冊邏輯,而 BeanFactory 則抽象出了 bean 的管理邏輯,而各個 BeanFactory 的實現類就具體承擔了 bean 的註冊以及管理工作。它們之間的關係就如下圖:

 

 

BeanFactory、BeanDefinitionRegistry 關係圖(來自:Spring 揭祕)

 

DefaultListableBeanFactory 作為一個比較通用的 BeanFactory 實現,它同時也實現了 BeanDefinitionRegistry 介面,因此它就承擔了 Bean 的註冊管理工作。

 

下面通過一段簡單的程式碼來模擬 BeanFactory 底層是如何工作的:

 

 

Spring IoC 容器的整個工作流程大致可以分為兩個階段:

 

①、容器啟動階段

容器啟動時,會通過某種途徑載入 ConfigurationMetaData。除了程式碼方式比較直接外,在大部分情況下,容器需要依賴某些工具類,來看一個簡單的例子吧,過往,所有的 bean 都定義在 XML 配置檔案中,下面的程式碼將模擬 BeanFactory 如何從配置檔案中載入 bean 的定義以及依賴關係:

 

 

 

②、Bean 的例項化階段

經過第一階段,所有bean定義都通過BeanDefinition的方式註冊到 BeanDefinitionRegistry 中,當某個請求通過容器的 getBean 方法請求某個物件,或者因為依賴關係容器需要隱式的呼叫 getBean 時,就會觸發第二階段的活動:容器會首先檢查所請求的物件之前是否已經例項化完成。

 

在實際場景下,我們更多的使用另外一種型別的容器: ApplicationContext,它構建在 BeanFactory 之上,屬於更高階的容器,除了具有 BeanFactory 的所有能力之外,還提供對事件監聽機制以及國際化的支援等。它管理的 bean,在容器啟動時全部完成初始化和依賴注入操作。

 

1.2、Spring 容器擴充套件機制

IoC 容器負責管理容器中所有 bean 的生命週期,而在 bean 生命週期的不同階段,Spring 提供了不同的擴充套件點來改變 bean 的命運。在容器的啟動階段, BeanFactoryPostProcessor 允許我們在容器例項化相應物件之前,對註冊到容器的 BeanDefinition 所儲存的資訊做一些額外的操作,比如修改 bean 定義的某些屬性或者增加其他資訊等。

 

如果要自定義擴充套件類,通常需要實現 org.springframework.beans.factory.config.BeanFactoryPostProcessor 介面,與此同時,因為容器中可能有多個 BeanFactoryPostProcessor,可能還需要實現 org.springframework.core.Ordered 介面,以保證BeanFactoryPostProcessor 按照順序執行。Spring 提供了為數不多的 BeanFactoryPostProcessor 實現,我們以 PropertyPlaceholderConfigurer 來說明其大致的工作流程。

 

在 Spring 專案的 XML 配置檔案中,經常可以看到許多配置項的值使用佔位符,而將佔位符所代表的值單獨配置到獨立的 properties 檔案,這樣可以將散落在不同 XML 檔案中的配置集中管理,而且也方便運維根據不同的環境進行配置不同的值。這個非常實用的功能就是由 PropertyPlaceholderConfigurer 負責實現的。

 

根據前文,當 BeanFactory 在第一階段載入完所有配置資訊時,BeanFactory 中儲存的物件的屬性還是以佔位符方式存在的,比如 ${jdbc.mysql.url}。當PropertyPlaceholderConfigurer 作為 BeanFactoryPostProcessor 被應用時,它會使用 properties 配置檔案中的值來替換相應的 BeanDefinition 中佔位符所表示的屬性值。當需要例項化 bean 時,bean 定義中的屬性值就已經被替換成我們配置的值。

 

跟BeanFactoryPostProcessor 類似,它會處理容器內所有符合條件並且已經例項化後的物件。簡單的對比,BeanFactoryPostProcessor 處理 bean的定義,而BeanPostProcessor 則處理 bean 完成例項化後的物件。BeanPostProcessor 定義了兩個介面:

 

 

 

為了理解這兩個方法執行的時機,簡單的瞭解下 bean 的整個生命週期:

 

Bean的例項化過程(來自:Spring揭祕)

 

postProcessBeforeInitialization()方法與 postProcessAfterInitialization()分別對應圖中前置處理和後置處理兩個步驟將執行的方法。再來看一個更常見的例子,在 Spring 中經常能夠看到各種各樣的 Aware 介面,其作用就是在物件例項化完成以後將 Aware 介面定義中規定的依賴注入到當前例項中。

 

比如最常見的 ApplicationContextAware 介面,實現了這個介面的類都可以獲取到一個 ApplicationContext 物件。當容器中每個物件的例項化過程走到 BeanPostProcessor 前置處理這一步時,容器會檢測到之前註冊到容器的ApplicationContextAwareProcessor,然後就會呼叫其 postProcessBeforeInitialization()方法,檢查並設定 Aware 相關依賴。看看程式碼吧,是不是很簡單:

 

 

 

理解這部分內容,足以讓您輕鬆理解 Spring Boot 的啟動原理,如果在後續的學習過程中遇到一些晦澀難懂的知識,再回過頭來看看 Spring 的核心知識,也許有意想不到的效果。

 

二、夯實基礎:JavaConfig 與常見 Annotation

 

2.1、JavaConfig

我們知道 bean 是 Spring IOC 中非常核心的概念,Spring 容器負責 bean 的生命週期的管理。在最初,Spring 使用 XML 配置檔案的方式來描述 bean 的定義以及相互間的依賴關係,但隨著 Spring 的發展,越來越多的人對這種方式表示不滿,因為 Spring 專案的所有業務類均以 bean 的形式配置在 XML 檔案中,造成了大量的 XML 檔案,使專案變得複雜且難以管理。

 

後來,基於純 Java Annotation 依賴注入框架 Guice 出世,其效能明顯優於採用 XML 方式的 Spring,甚至有部分人認為, Guice 可以完全取代 Spring( Guice僅是一個輕量級 IOC 框架,取代 Spring 還差的挺遠)。正是這樣的危機感,促使 Spring 及社群推出並持續完善了 JavaConfig 子專案,它基於 Java 程式碼和 Annotation 註解來描述 bean 之間的依賴繫結關係。比如,下面是使用 XML 配置方式來描述 bean 的定義:

 

 

 

而基於 JavaConfig 的配置形式是這樣的:

 

如果兩個 bean 之間有依賴關係的話,在 XML 配置中應該是這樣:

 

而在 JavaConfig 中則是這樣:

  

你可能注意到這個示例中,有兩個 bean 都依賴於 dependencyService,也就是說當初始化 bookService 時會呼叫 dependencyService(),在初始化 otherService 時也會呼叫 dependencyService()。

 

2.2、@ComponentScan

@ComponentScan 註解對應 XML 配置形式中的 <context:component-scan>元素,表示啟用元件掃描,Spring 會自動掃描所有通過註解配置的 bean,然後將其註冊到 IOC 容器中。我們可以通過 basePackages 等屬性來指定 @ComponentScan 自動掃描的範圍,如果不指定,預設從宣告 @ComponentScan 所在類的 package 進行掃描。正因為如此,SpringBoot 的啟動類都預設在 src/main/java 下。

 

2.3、@Import

@Import 註解用於匯入配置類,舉個簡單的例子:

 

 

 

現在有另外一個配置類,比如: MoonUserConfiguration,這個配置類中有一個bean 依賴於 MoonBookConfiguration 中的 bookService,如何將這兩個 bean 組合在一起?藉助 @Import 即可:

 

 

 

需要注意的是,在 4.2 之前, @Import 註解只支援匯入配置類,但是在 4.2 之後,它支援匯入普通類,並將這個類作為一個 bean 的定義註冊到 IOC 容器中。

 

2.4、@Conditional

@Conditional 註解表示在滿足某種條件後才初始化一個 bean 或者啟用某些配置。它一般用在由 @Component、 @Service、 @Configuration 等註解標識的類上面,或者由 @Bean 標記的方法上。如果一個 @Configuration 類標記了 @Conditional,則該類中所有標識了 @Bean 的方法和 @Import 註解匯入的相關類將遵從這些條件。

 

在 Spring 裡可以很方便的編寫你自己的條件類,所要做的就是實現 Condition 介面,並覆蓋它的 matches()方法。舉個例子,下面的簡單條件類表示只有在  Classpath 裡存在 JdbcTemplate 類時才生效:

 

當你用 Java 來宣告 bean 的時候,可以使用這個自定義條件類:

 

這個例子中只有當 JdbcTemplateCondition 類的條件成立時才會建立 MyService 這個 bean。也就是說 MyService 這 bean 的建立條件是 classpath 裡面包含  JdbcTemplate,否則這個 bean 的宣告就會被忽略掉。

 

SpringBoot 定義了很多有趣的條件,並把他們運用到了配置類上,這些配置類構成了 SpringBoot 的自動配置的基礎。 SpringBoot 運用條件化配置的方法是:定義多個特殊的條件化註解,並將它們用到配置類上。下面列出了 SpringBoot 提供的部分條件化註解:

2.5、@ConfigurationProperties 與@EnableConfigurationProperties

當某些屬性的值需要配置的時候,我們一般會在 application.properties 檔案中新建配置項,然後在 bean 中使用 @Value 註解來獲取配置的值,比如下面配置資料來源的程式碼。

 

使用 @Value 註解注入的屬性通常都比較簡單,如果同一個配置在多個地方使用,也存在不方便維護的問題(考慮下,如果有幾十個地方在使用某個配置,而現在你想改下名字,你改怎麼做?)。對於更為複雜的配置,Spring Boot 提供了更優雅的實現方式,那就是 @ConfigurationProperties 註解。我們可以通過下面的方式來改寫上面的程式碼:

 

@ConfigurationProperties 對於更為複雜的配置,處理起來也是得心應手,比如有如下配置檔案:

 

 

可以定義如下配置類來接收這些屬性

 

@EnableConfigurationProperties 註解表示對 @ConfigurationProperties 的內嵌支援,預設會將對應 Properties Class 作為 bean 注入的 IOC 容器中,即在相應的 Properties 類上不用加 @Component 註解。

 

三、削鐵如泥:SpringFactoriesLoader 詳解

 

JVM 提供了 3 種類載入器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader 分別載入 Java 核心類庫、擴充套件類庫以及應用的類路徑( CLASSPATH)下的類庫。JVM通過雙親委派模型進行類的載入,我們也可以通過繼承  java.lang.classloader 實現自己的類載入器。

 

何為雙親委派模型?當一個類載入器收到類載入任務時,會先交給自己的父載入器去完成,因此最終載入任務都會傳遞到最頂層的 BootstrapClassLoader,只有當父載入器無法完成載入任務時,才會嘗試自己來載入。

 

採用雙親委派模型的一個好處是保證使用不同類載入器最終得到的都是同一個物件,這樣就可以保證 Java 核心庫的型別安全。檢視 ClassLoader 的原始碼,對雙親委派模型會有更直觀的認識:

 

 

 

但雙親委派模型並不能解決所有的類載入器問題,比如,Java 提供了很多服務提供者介面,允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JNDI、JAXP 等,這些 SPI 的介面由核心類庫提供,卻由第三方實現,這樣就存在一個問題:

 

SPI 的介面是 Java 核心庫的一部分,是由 BootstrapClassLoader 載入的;SPI 實現的 Java 類一般是由 AppClassLoader 來載入的。BootstrapClassLoader 是無法找到 SPI 的實現類的,因為它只加載 Java 的核心庫。它也不能代理給 AppClassLoader,因為它是最頂層的類載入器。也就是說,雙親委派模型並不能解決這個問題。

 

執行緒上下文類載入器( ContextClassLoader)正好解決了這個問題。如果不做任何的設定,Java 應用的執行緒的上下文類載入器預設就是 AppClassLoader。在核心類庫使用 SPI 介面時,傳遞的類載入器使用執行緒上下文類載入器,就可以成功的載入到 SPI 實現的類。

 

執行緒上下文類載入器在很多 SPI 的實現中都會用到。但在 JDBC 中,你可能會看到一種更直接的實現方式,比如,JDBC 驅動管理 java.sql.Driver 中的 loadInitialDrivers()方法中,你可以直接看到 JDK 是如何載入驅動的:

 

其實講解執行緒上下文類載入器,最主要是讓大家在看到 Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader()時不會一臉懵逼,這兩者除了在許多底層框架中取得的 ClassLoader 可能會有所不同外,其他大多數業務場景下都是一樣的,大家只要知道它是為了解決什麼問題而存在的即可。

類載入器除了載入 class 外,還有一個非常重要功能,就是載入資源,它可以從jar包中讀取任何資原始檔,比如, ClassLoader.getResources(Stringname)方法就是用於讀取 jar 包中的資原始檔,其程式碼如下:

 

是不是覺得有點眼熟,不錯,它的邏輯其實跟類載入的邏輯是一樣的,首先判斷父類載入器是否為空,不為空則委託父類載入器執行資源查詢任務,直到 BootstrapClassLoader,最後才輪到自己查詢。而不同的類載入器負責掃描不同路徑下的 jar 包,就如同載入 class 一樣,最後會掃描所有的 jar 包,找到符合條件的資原始檔。

類載入器的 findResources(name)方法會遍歷其負責載入的所有 jar 包,找到 jar 包中名稱為 name 的資原始檔,這裡的資源可以是任何檔案,甚至是 .clas 檔案,比如下面的示例,用於查詢 Array.class 檔案:

 

執行後可以得到如下結果:

 

根據資原始檔的 URL,可以構造相應的檔案來讀取資源內容。

 

看到這裡,你可能會感到挺奇怪的,你不是要詳解 SpringFactoriesLoader 嗎?上來講了一堆 ClassLoader 是幾個意思?看下它的原始碼你就知道了:

  

有了前面關於 ClassLoader 的知識,再來理解這段程式碼,是不是感覺豁然開朗:從 CLASSPATH 下的每個 Jar 包中搜尋所有 META-INF/spring.factories 配置檔案,然後將解析 properties 檔案,找到指定名稱的配置後返回。

 

需要注意的是,其實這裡不僅僅是會去 ClassPath 路徑下查詢,會掃描所有路徑下的 Jar 包,只不過這個檔案只會在 Classpath 下的 jar 包中。來簡單看下 spring.factories 檔案的內容吧:

 

 

執行 loadFactoryNames(EnableAutoConfiguration.class,classLoader)後,得到對應的一組 @Configuration 類,我們就可以通過反射例項化這些類然後注入到 IOC 容器中,最後容器裡就有了一系列標註了 @Configuration 的 JavaConfig 形式的配置類。

 

這就是 Spring Boot 啟動流程的上半部分,其核心就是在 Spring 容器初始化並啟動的基礎上加入各種擴充套件點,這些擴充套件點包括:ApplicationContextInitializer、ApplicationListener 以及各種 BeanFactoryPostProcessor 等。

 

四、另一件武器:Spring 容器的事件監聽機制

 

過去,事件監聽機制多用於圖形介面程式設計,比如:點選按鈕、在文字框輸入內容等操作被稱為事件,而當事件觸發時,應用程式作出一定的響應則表示應用監聽了這個事件,而在伺服器端,事件的監聽機制更多的用於非同步通知以及監控和異常處理。

 

Java提供了實現事件監聽機制的兩個基礎類:自定義事件型別擴充套件自 java.util.EventObject、事件的監聽器擴充套件自 java.util.EventListener。

 

五、啟動引導:Spring Boot 應用啟動的祕密

 

5.1 SpringApplication 初始化

SpringBoot 整個啟動流程分為兩個步驟:初始化一個 SpringApplication 物件、執行該物件的 run 方法。看下 SpringApplication 的初始化流程, SpringApplication 的構造方法中呼叫 initialize(Object[] sources)方法,其程式碼如下:

5.2 Spring Boot 啟動流程

Spring Boot 應用的整個啟動流程都封裝在 SpringApplication.run 方法中,其整個流程真的是太長太長了,但本質上就是在 Spring 容器啟動的基礎上做了大量的擴充套件,按照這個思路來看看原始碼:

 

這就是 Spring Boot 的整個啟動流程,其核心就是在 Spring 容器初始化並啟動的基礎上加入各種擴充套件點,如果你想更加詳細的瞭解整個流程,或者理解這些擴充套件點是在何時如何工作的,並能讓它們為你所用

 

另一篇搭建SpringMVC的部落格 https://www.cnblogs.com/waycx/p/9665923.html

 

寫留言