SpringBoot:認認真真梳理一遍自動裝配原理

前言

Spring翻譯為中文是“春天”,的確,在某段時間內,它給Java開發人員帶來過春天,但是隨著我們項目規模的擴大,Spring需要配置的地方就越來越多,誇張點說,“配置兩小時,Coding五分鐘”。

這種紛繁複雜的xml配置隨著軟件行業一步步地發展,必將逐步退出歷史舞臺。


SpringBoot介紹

Spring Boot是由Pivotal團隊提供的全新框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程。該框架使用了特定的方式來進行配置,從而使開發人員不再需要定義樣板化的配置。

通過這種方式,Spring Boot致力於在蓬勃發展的快速應用開發領域(rapid application development)成為領導者。

SpringBoot所具備的特徵有:

  • 可以創建獨立的Spring應用程序,並且基於其Maven或Gradle插件,可以創建可執行的JARs和WARs;
  • 內嵌Tomcat或Jetty等Servlet容器;
  • 提供自動配置的“starter”項目對象模型(POMS)以簡化Maven配置;
  • 儘可能自動配置Spring容器;
  • 提供準備好的特性,如指標、健康檢查和外部化配置;
  • 絕對沒有代碼生成,不需要XML配置。

自己的理解:

SpringBoot,顧名思義,給人的感覺就是讓Spring啟動的這麼一個項目。在過去,我們要讓一個Spring項目啟動,往往需要配置很多的xml配置文件,但是在使用SpringBoot之後,我們甚至無需寫一行xml,就可以直接將整個項目啟動

這種“零配置”的做法減輕了開發人員很多的工作量,可以讓開發人員一心撲在業務邏輯的設計上,使項目的邏輯更加完善。

除此之外,其採用了JavaConfig的配置風格,導入組件的方式也由原來的直接配置改為@EnableXXXX,這種純Java代碼的配置和導入組件的方式,使代碼看上去更加的優雅,所以SpringBoot如今受到大小公司和大多數程序員的青睞,不是沒有原因的。

SpringBoot之所以可以做到簡化配置文件直接啟動,無外乎是其內部的兩種設計策略:開箱即用和約定大於配置

開箱即用:在開發過程中,通過maven項目的pom文件中添加相關依賴包,然後通過相應的註解來代替繁瑣的XML配置以管理對象的生命週期。

約定大於配置:由SpringBoot本身來配置目標結構,由開發者在結構中添加信息的軟件設計範式。

這一特點雖降低了部分靈活性,增加了BUG定位的複雜性,但減少了開發人員需要做出決定的數量,同時減少了大量的XML配置,並且可以將代碼編譯、測試和打包等工作自動化。

那麼在這篇博客中,我們需要了解的所有東西,就應該從這兩個特點出發,一步一步深入SpringBoot自動裝配的原理。


開箱即用原理

要理解這一特點,首先要先自己體會開箱即用的整個過程帶來的便利。

體驗開箱即用

SpringBoot提供了我們快速創建SpringBoot項目的地方:https://start.spring.io/

我們只需要在這個網頁中把整個項目起好名字,然後選好我們需要的組件,就可以直接獲得一個可以跑起來的SpringBoot項目。

SpringBoot:認認真真梳理一遍自動裝配原理

我們只需要填完上述信息,點擊Generate,就可以直接將一個SpringBoot項目下載下來,然後導入我們的IDE,Eclipse或者IDEA都可,之後就可以直接將它運行起來。

全項目結構:

SpringBoot:認認真真梳理一遍自動裝配原理

啟動:

SpringBoot:認認真真梳理一遍自動裝配原理

訪問:http://localhost:8080/

SpringBoot:認認真真梳理一遍自動裝配原理

代表整個SpringBoot項目啟動成功。

開箱即用原理剖析

對比SSM配置

其實在上文的開箱即用中,我們相當於引入了一個SpringMVC的組件,但是大家可以看到,我們沒有經過任何的配置就將項目啟動了。反觀過去SSM框架的SpringMVC配置,我這裡有一份留存的大家可以對比一下。

spring-web.xml:


<beans> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">



<annotation-driven>


<resources>
<default-servlet-handler>


<bean> class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property>
<property>
/<bean>

<bean> class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property>
<property>
<property>
/<bean>

<bean> class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property>
<list>
<bean> class="org.springframework.http.converter.StringHttpMessageConverter">
<property>
<list>
<value>text/html;charset=UTF-8/<value>
/<list>
/<property>
/<bean>
/<list>
/<property>
/<bean>

<component-scan>

/<beans>

web.xml:

 <servlet>
<servlet-name>spring-dispatcher/<servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet/<servlet-class>

<init-param>
<param-name>contextConfigLocation/<param-name>
<param-value>classpath:spring/spring-*.xml/<param-value>
/<init-param>
/<servlet>
<servlet-mapping>
<servlet-name>spring-dispatcher/<servlet-name>

<url-pattern>//<url-pattern>
/<servlet-mapping>

可以看到,這裡需要配置兩個文件,web.xml和spring-web.xml,配置可以說是相當繁重。

那麼相對於這個,SpringBoot的開箱即用就顯得特別方便,那麼我們著重聊聊SpringBoot開箱即用的原理。

從pom.xml開始

SpringBoot的項目都會存在一個父依賴,按住Ctrl+鼠標左鍵,可以點進去。

<parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-parent/<artifactid>
<version>2.2.1.RELEASE/<version>
<relativepath>
/<parent>

點進去之後發現裡面除了一些插件和配置文件的格式之外,還存在一個依賴。

<parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-dependencies/<artifactid>
<version>2.2.1.RELEASE/<version>

<relativepath>../../spring-boot-dependencies/<relativepath>
/<parent>

於是再點進去,可以發現裡面放了很多的依賴和依賴的版本號。由於這個文件實在太長了,所以這裡只展示一部分。

SpringBoot:認認真真梳理一遍自動裝配原理


SpringBoot:認認真真梳理一遍自動裝配原理

所以我們可以得出第一個結論:

spring-boot-dependencies:作為父工程,存放了SpringBoot的核心依賴。我們在寫或者引入一些SpringBoot依賴的時候,不需要指定版本,正是因為SpringBoot的父依賴已經幫我們維護了一套版本。

另外我們還可以看到,在父依賴中也幫我們寫好了資源庫,不用我們自己再去配置了。

<resources>
<resource>
<filtering>true/<filtering>
<directory>${basedir}/src/main/resources/<directory>
<includes>

<include>**/application*.yml/<include>
<include>**/application*.yaml/<include>
<include>**/application*.properties/<include>
/<includes>
/<resource>
<resource>
<directory>${basedir}/src/main/resources/<directory>
<excludes>
<exclude>**/application*.yml/<exclude>
<exclude>**/application*.yaml/<exclude>
<exclude>**/application*.properties/<exclude>
/<excludes>
/<resource>
/<resources>

啟動器

<dependency> 

<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter/<artifactid>
<version>2.2.1.RELEASE/<version>
/<dependency>

啟動器就是SpringBoot的啟動場景,比如我們要使用web相關的,那麼就直接引入spring-boor-starter-web,那麼他就會幫我們自動導入web環境下所有必需的依賴。

我們來看看啟動器中存放了一些什麼內容:

以spring-boot-starter為例:

 <dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot/<artifactid>
<version>2.2.1.RELEASE/<version>
<scope>compile/<scope>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-autoconfigure/<artifactid>
<version>2.2.1.RELEASE/<version>
<scope>compile/<scope>
/<dependency>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-logging/<artifactid>
<version>2.2.1.RELEASE/<version>
<scope>compile/<scope>
/<dependency>
<dependency>
<groupid>jakarta.annotation/<groupid>
<artifactid>jakarta.annotation-api/<artifactid>
<version>1.3.5/<version>
<scope>compile/<scope>
/<dependency>
<dependency>
<groupid>org.springframework/<groupid>
<artifactid>spring-core/<artifactid>
<version>5.2.1.RELEASE/<version>
<scope>compile/<scope>
/<dependency>
<dependency>
<groupid>org.yaml/<groupid>

<artifactid>snakeyaml/<artifactid>
<version>1.25/<version>
<scope>runtime/<scope>
/<dependency>

其中存放了自動配置相關的依賴、日誌相關依賴、還有Spring-core等依賴,這些依賴我們只需要導入一個spring-boor-starter就可以直接將其全部引入,而不需要再像以前那樣逐個導入了。

SpringBoot會將所有的功能場景都封裝成一個一個的啟動器,供開發人員使用。

我們在使用的時候也可以直接去官網上找我們所需的啟動器,直接將其引入。

獲取啟動器文檔:

https://docs.spring.io/spring-boot/docs/2.2.1.RELEASE/reference/html/using-spring-boot.html#using-boot-starter

主程序(重要)

//@SpringBootApplication 標註,是一個SpringBoot應用
@SpringBootApplication
public class SpringbootdemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootdemoApplication.class, args);
}
}

再寫SpringBoot項目的時候,總要寫這麼一個主程序,這個主程序最大的特點就是其類上放了一個@SpringBootApplication註解,這也正是SpringBoot項目啟動的核心,也是我們要研究的重點。

注意:之後的分析可能會深入源碼,源碼是一層一層嵌套的,所以光靠文字說明會比較難以理解,最好是自己在IDE環境下跟著一步一步跟著點下去。當然也可以繞過這一部分直接看結論。

點開@SpringBootApplication,可以發現它是一個組合註解,主要是由這麼幾個註解構成的。

@SpringBootConfiguration//核心
@EnableAutoConfiguration//核心
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

我們首先要研究的就是核心的兩個註解 @SpringBootConfiguration@EnableAutoConfiguration,逐個進行分析。

@SpringBootConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}

可以看到SpringBootConfiguration其實就攜帶了一個@Configuration註解,這個註解我們再熟悉不過了,他就代表自己是一個Spring的配置類。所以我們可以認為:@SpringBootConfiguration = @Configuration

@EnableAutoConfiguration

顧名思義,這個註解一定和自動配置相關,點進去看源代碼之後可以發現,其內部就包含了這麼兩個註解。

@AutoConfigurationPackage //自動配置包
@Import(AutoConfigurationImportSelector.class)//自動配置導入選擇

來看看@Import(AutoConfigurationImportSelector.class)中的內容:

它幫我們導入了AutoConfigurationImportSelector,這個類中存在一個方法可以幫我們獲取所有的配置,代碼如下。

/*
所有的配置都存放在configurations中,
而這些配置都從getCandidateConfiguration中獲取,
這個方法是用來獲取候選的配置。
*/
List<string> configurations = getCandidateConfigurations(annotationMetadata, attributes);
/<string>

getCandidateConfigurations():

這個方法可以用來獲取所有候選的配置,那麼這些候選的配置又是從哪來的呢?

/*獲取候選的配置*/
protected List<string> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<string> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),

getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
/<string>/<string>

實際上它返回了一個List,這個List是由loadFactoryNames()方法返回的,其中傳入了一個getSpringFactoriesLoaderFactoryClass(),我們可以看看這個方法的內容。

protected Class getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}

我們看到了一個眼熟的詞 —— EnableAutoConfiguration,也就是說,它實際上返回的就是標註了這個類的所有包。標註了這個類的包不就是@SpringBootApplication嗎?

所以我們可以得出結論:它兜兜轉轉繞了這麼多地方,就是為了將啟動類所需的所有資源導入。

我們接著往下看,它其中還有這麼一條語句,是一條斷言:

Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");

這個斷言的意思是,configurations必須非空,否則就打印一段話,No auto configuration classes found in META-INF/spring.factories,我們把這個邏輯反過來想想。如果這個集合不為空,是不是就代表找到了這個spring.factories並且會去加載這個文件中的內容呢?

帶著這個疑問,我們首先找到spring.factories這個文件:

SpringBoot:認認真真梳理一遍自動裝配原理

可以看到裡面包含了很多自動配置屬性:

SpringBoot:認認真真梳理一遍自動裝配原理

我們可以隨便找一個自動配置點進去,比如WebMvcAutoConfiguration:

SpringBoot:認認真真梳理一遍自動裝配原理

這裡放了所有關於WebMvc的配置,如視圖解析器、國際化等等。

分析到這裡,我們就可以得出一個完整的結論了:

當我們的SpringBoot項目啟動的時候,會先導入AutoConfigurationImportSelector

這個類會幫我們選擇所有候選的配置,我們需要導入的配置都是SpringBoot幫我們寫好的一個一個的配置類,那麼這些配置類的位置,存在與META-INF/spring.factories文件中

通過這個文件,Spring可以找到這些配置類的位置,於是去加載其中的配置。

SpringBoot:認認真真梳理一遍自動裝配原理

看到這裡,可能有些同學會存在疑問,spring.factories中存在那麼多的配置,每次啟動時都是把它們全量加載嗎?這顯然是不現實的。

這其實也是我在看源碼的時候存在疑問的地方,因為其中有一個註解並不常用,我們點開一個配置類就可以看到。

SpringBoot:認認真真梳理一遍自動裝配原理

@ConditionalOnXXX:如果其中的條件都滿足,該類才會生效。

所以在加載自動配置類的時候,並不是將spring.factories的配置全量加載進來,而是通過這個註解的判斷,如果註解中的類都存在,才會進行加載。

所以就實現了:我們在pom.xml文件中加入stater啟動器,SpringBoot自動進行配置。完成開箱即用。

結論

SpringBoot所有自動配置類都是在啟動的時候進行掃描並加載,通過spring.factories可以找到自動配置類的路徑,但是不是所有存在於spring,factories中的配置都進行加載,而是通過@ConditionalOnClass註解進行判斷條件是否成立(只要導入相應的stater,條件就能成立),如果條件成立則加載配置類,否則不加載該配置類。

在這裡貼一個我認為的比較容易理解的過程:

  • SpringBoot在啟動的時候從類路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值
  • 將這些值作為自動配置類導入容器 , 自動配置類就生效 , 幫我們進行自動配置工作;
  • 以前我們需要自己配置的東西 , 自動配置類都幫我們解決了
  • 整個J2EE的整體解決方案和自動配置都在springboot-autoconfigure的jar包中;
  • 它將所有需要導入的組件以全類名的方式返回 , 這些組件就會被添加到容器中 ;
  • 它會給容器中導入非常多的自動配置類 (xxxAutoConfiguration), 就是給容器中導入這個場景需要的所有組件 , 並配置好這些組件 ;
  • 有了自動配置類 , 免去了我們手動編寫配置注入功能組件等的工作;

摘自https://blog.kuangstudy.com/index.php/archives/630/


SpringBoot:認認真真梳理一遍自動裝配原理

約定大於配置

開箱即用的原理說完了,約定大於配置就比較好理解了。其實約定大於配置就是開箱即用中那些自動配置的細節。

說的具體點就是:我們的配置文件(.yml)應該放在哪個目錄下,配置文件的命名規範,項目啟動時掃描的Bean,組件的默認配置是什麼樣的(比如SpringMVC的視圖解析器)等等等等這一系列的東西,都可以被稱為約定

下面就來一點一點地說一下SpringBoot中的“約定”。

maven目錄結構的約定

我們可以去Spring的官網查看一下官方文檔,看看文檔中描述的目錄結構是怎樣的。

Config locations are searched in reverse order. By default, the configured locations are classpath:/,classpath:/config/,file:./,file:./config/. The resulting search order is the following:

  • file:./config/
  • file:./
  • classpath:/config/
  • classpath:/

也就是說,spring的配置文件目錄可以放在

  • /config
  • /(根目錄)
  • resource/config/
  • resource/

這四個路徑從上到下存在優先級關係。

SpringBoot默認配置文件的約定

SpringBoot默認可以加載以下三種配置文件:

  • application.yml
  • application.yaml
  • application.properties

建議使用前兩種作為項目的配置文件。

項目啟動時掃描包範圍的約定

SpringBoot的註解掃描的默認規則是SpringBoot的入口類所在包及其子包。

若入口類所在的包是cn.objectspace.demo那麼自動掃描包的範圍是cn.objectspace.demo包及其下面的子包,如果service包和dao包不在此範圍,則不會自動掃描。


SpringBoot自動配置類如何讀取yml配置

從更細節的角度去理解自動配置

上文中我們闡述了一些SpringBoot自動配置的原理,我們是從全局的角度去看自動配置的整個過程。比如從哪個地方開始進行裝配流程、如何找到裝配的包等。

那麼現在將自己的視角貼近SpringBoot,來聊聊application.yml中我們配置的東西,是如何配置到一個個的配置類中的。

yml配置文件中可以配置那些東西

首先要知道這個問題的答案,我們應該習慣springboot的配置方式。在上文中我們闡述了SpringBoot總是將所有的配置都用JavaConfig的形式去呈現出來,這樣能夠使代碼更加優雅。

那麼yml中配置的東西,必然是要和這種配置模式去進行聯繫的,我們在application.yml中配置的東西,通常是一些存在與自動配置類中的屬性,那麼這些自動配置類,在啟動的時候是怎麼找到的呢?

如果你還記得上文的描述,那麼你可以很明確地知道:spring.factories!沒錯,就是它,所以這個問題我們似乎得到了答案——只要存在與spring.factories中的,我們都可以在application.yml中進行配置。

當然,這並不意味著不存在其中的我們就不能配置,這些配置類我們是可以進行自定義的

只要我們寫了配置類,我們就可以在yml中配置我們需要的屬性值,然後在配置類中直接讀取這個配置文件,將其映射到配置類的屬性上。

那麼就牽扯出我們的問題了:配置類是如何去讀取yml配置文件中的信息的呢?

@ConfigurationProperties

要明白這個問題。我們就首先要去了解這個註解有什麼作用。

我們可以自己嘗試在application.yml中去定義一些屬性,如下:

object: 
name: Object
blogurl: blog.objectspace.cn

我們現在自己定義一個類去讀取這個文件:

@Component
@ConfigurationProperties(prefix = "object")
public class TestConfig {
private String name;
private String blogUrl;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBlogUrl() {
return blogUrl;
}
public void setBlogUrl(String blogUrl) {
this.blogUrl = blogUrl;
}
}

然後我們在測試類中輸出一下這個對象:

@SpringBootTest 

class SpringbootdemoApplicationTests {
@Autowired
TestConfig testConfig;
@Test
void contextLoads() {
System.out.println(testConfig.getName());
System.out.println(testConfig.getBlogUrl());
}

}

測試結果:

SpringBoot:認認真真梳理一遍自動裝配原理

我們可以看到,在控制檯中輸出了我們在yml中配置的屬性值,但是這些值我們沒有在任何地方顯式地對這個對象進行注入。

所以@ConfigurationProperties這個註解,可以將yml文件中寫好的值注入到我們類的屬性中。

明白了它的作用,就能明白自動配置類工作的原理了。

我們依舊是選取SpringMVC的自動配置類,我們來看看其中有些什麼東西。

SpringBoot:認認真真梳理一遍自動裝配原理

點擊任意一個*Properties類中,look一下其中的內容:

SpringBoot:認認真真梳理一遍自動裝配原理

看到這裡相信所有人都明白了,我們就拿mvc配置來舉例。

SpringBoot:認認真真梳理一遍自動裝配原理

我們在yml中配置的date-format,就可以通過@ConfigurationProperties映射到類中的dateFormat中,然後在通過自動配置類,將這些屬性配置到配置類中。


分享到:


相關文章: