前言
Spring翻譯為中文是“春天”,的確,在某段時間內(nèi),它給Java開發(fā)人員帶來過春天,但是隨著我們項目規(guī)模的擴大,Spring需要配置的地方就越來越多,夸張點說,“配置兩小時,Coding五分鐘”。這種紛繁復(fù)雜的xml配置隨著軟件行業(yè)一步步地發(fā)展,必將逐步退出歷史舞臺。
SpringBoot介紹
來自:百度百科
Spring Boot是由Pivotal團隊提供的全新框架,其設(shè)計目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。該框架使用了特定的方式來進行配置,從而使開發(fā)人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力于在蓬勃發(fā)展的快速應(yīng)用開發(fā)領(lǐng)域(rapid application development)成為領(lǐng)導(dǎo)者。SpringBoot所具備的特征有:
(1)可以創(chuàng)建獨立的Spring應(yīng)用程序,并且基于其Maven或Gradle插件,可以創(chuàng)建可執(zhí)行的JARs和WARs;
(2)內(nèi)嵌Tomcat或Jetty等Servlet容器;
(3)提供自動配置的“starter”項目對象模型(POMS)以簡化Maven配置;
(4)盡可能自動配置Spring容器;
(5)提供準備好的特性,如指標(biāo)、健康檢查和外部化配置;
(6)絕對沒有代碼生成,不需要XML配置。
自己的理解:
SpringBoot,顧名思義,給人的感覺就是讓Spring啟動的這么一個項目。在過去,我們要讓一個Spring項目啟動,往往需要配置很多的xml配置文件,但是在使用SpringBoot之后,我們甚至無需寫一行xml,就可以直接將整個項目啟動,這種“零配置”的做法減輕了開發(fā)人員很多的工作量,可以讓開發(fā)人員一心撲在業(yè)務(wù)邏輯的設(shè)計上,使項目的邏輯更加完善。除此之外,其采用了JavaConfig的配置風(fēng)格,導(dǎo)入組件的方式也由原來的直接配置改為@EnableXXXX,這種純Java代碼的配置和導(dǎo)入組件的方式,使代碼看上去更加的優(yōu)雅,所以SpringBoot如今受到大小公司和大多數(shù)程序員的青睞,不是沒有原因的。
SpringBoot之所以可以做到簡化配置文件直接啟動,無外乎是其內(nèi)部的兩種設(shè)計策略:開箱即用和約定大于配置。
開箱即用:在開發(fā)過程中,通過maven項目的pom文件中添加相關(guān)依賴包,然后通過相應(yīng)的注解來代替繁瑣的XML配置以管理對象的生命周期。
約定大于配置:由SpringBoot本身來配置目標(biāo)結(jié)構(gòu),由開發(fā)者在結(jié)構(gòu)中添加信息的軟件設(shè)計范式。這一特點雖降低了部分靈活性,增加了BUG定位的復(fù)雜性,但減少了開發(fā)人員需要做出決定的數(shù)量,同時減少了大量的XML配置,并且可以將代碼編譯、測試和打包等工作自動化。
那么在這篇博客中,我們需要了解的所有東西,就應(yīng)該從這兩個特點出發(fā),一步一步深入SpringBoot自動裝配的原理。
開箱即用原理
要理解這一特點,首先要先自己體會開箱即用的整個過程帶來的便利。
-
體驗開箱即用
SpringBoot提供了我們快速創(chuàng)建SpringBoot項目的地方:https://start.spring.io/
我們只需要在這個網(wǎng)頁中把整個項目起好名字,然后選好我們需要的組件,就可以直接獲得一個可以跑起來的SpringBoot項目。
官網(wǎng)生成我們只需要填完上述信息,點擊Generate,就可以直接將一個SpringBoot項目下載下來,然后導(dǎo)入我們的IDE,Eclipse或者IDEA都可,之后就可以直接將它運行起來。
全項目結(jié)構(gòu):
全項目結(jié)構(gòu)啟動:
項目啟動啟動成功代表整個SpringBoot項目啟動成功。
-
開箱即用原理剖析
-
對比SSM配置
其實在上文的開箱即用中,我們相當(dāng)于引入了一個SpringMVC的組件,但是大家可以看到,我們沒有經(jīng)過任何的配置就將項目啟動了。反觀過去SSM框架的SpringMVC配置,我這里有一份留存的大家可以對比一下。
spring-web.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" 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"> <!-- 配置SpringMVC --> <!-- 1.開啟SpringMVC注解模式 --> <!-- 簡化配置: (1)自動注冊DefaultAnootationHandlerMapping,AnotationMethodHandlerAdapter (2)提供一些列:數(shù)據(jù)綁定,數(shù)字和日期的format @NumberFormat, @DateTimeFormat, xml,json默認讀寫支持 --> <mvc:annotation-driven /> <!-- 2.靜態(tài)資源默認servlet配置 (1)加入對靜態(tài)資源的處理:js,gif,png (2)允許使用"/"做整體映射 --> <mvc:resources mapping="/resources/**" location="/resources/" /> <mvc:default-servlet-handler /> <!-- 3.定義視圖解析器 --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/html/"></property> <property name="suffix" value=".html"></property> </bean> <!-- 文件上傳解析器 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="defaultEncoding" value="utf-8"></property> <property name="maxUploadSize" value="10485760000"></property><!-- 最大上傳文件大小 --> <property name="maxInMemorySize" value="20971520"></property> </bean> <!-- 在spring-mvc.xml文件中加入這段配置后,spring返回給頁面的都是utf-8編碼了 --> <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes"> <list> <value>text/html;charset=UTF-8</value> </list> </property> </bean> </list> </property> </bean> <!-- 4.掃描web相關(guān)的bean --> <context:component-scan base-package="com.SchoolShop.o2o.web" /> <!-- 5.權(quán)限攔截器 --> </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,配置可以說是相當(dāng)復(fù)雜。
相對于這個,SpringBoot的開箱即用就顯得特別方便,那么我們著重聊聊SpringBoot開箱即用的原理。
-
從pom.xml開始
SpringBoot的項目都會存在一個父依賴,按住Ctrl+鼠標(biāo)左鍵,可以點進去。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>點進去之后發(fā)現(xiàn)里面除了一些插件和配置文件的格式之外,還存在一個依賴。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.1.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>于是再點進去,可以發(fā)現(xiàn)里面放了很多的依賴和依賴的版本號。由于這個文件實在太長了,所以這里只展示一部分。
父依賴父依賴包含所以我們可以得出第一個結(jié)論:
spring-boot-dependencies:作為父工程,存放了SpringBoot的核心依賴。我們在寫或者引入一些SpringBoot依賴的時候,不需要指定版本,正式因為SpringBoot的父依賴已經(jīng)幫我們維護了一套版本。
另外我們還可以看到,在父依賴中也幫我們寫好了資源庫,不用我們自己再去配置了。
<resources> <resource> <filtering>true</filtering> <directory>${basedir}/src/main/resources</directory> <includes> <!-- 可以讀取的配置文件有 application.yml/application.yaml/application.properties --> <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相關(guān)的,那么就直接引入spring-boot-starter-web,那么他就會幫我們自動導(dǎo)入web環(huán)境下所有必需的依賴。
我們來看看啟動器中存放了一些什么內(nèi)容:
以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>其中存放了自動配置相關(guān)的依賴、日志相關(guān)依賴、還有Spring-core等依賴,這些依賴我們只需要導(dǎo)入一個spring-boot-starter就可以直接將其全部引入,而不需要再像以前那樣逐個導(dǎo)入了。
SpringBoot會將所有的功能場景都封裝成一個一個的啟動器,供開發(fā)人員使用。
我們在使用的時候也可以直接去官網(wǎng)上找我們所需的啟動器,直接將其引入。
-
主程序(重要)
//@SpringBootApplication 標(biāo)注,是一個SpringBoot應(yīng)用 @SpringBootApplication public class SpringbootdemoApplication { public static void main(String[] args) { SpringApplication.run(SpringbootdemoApplication.class, args); } }在寫SpringBoot項目的時候,總要寫這么一個主程序,這個主程序最大的特點就是其類上放了一個@SpringBootApplication注解,這也正是SpringBoot項目啟動的核心,也是我們要研究的重點。
注意:之后的分析可能會深入源碼,源碼是一層一層嵌套的,所以光靠文字說明會比較難以理解,最好是自己在IDE環(huán)境下跟著一步一步跟著點下去。當(dāng)然也可以繞過這一部分直接看結(jié)論。
點開@SpringBootApplication,可以發(fā)現(xiàn)它是一個組合注解,主要是由這么幾個注解構(gòu)成的。
@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
顧名思義,這個注解一定和自動配置相關(guān),點進去看源代碼之后可以發(fā)現(xiàn),其內(nèi)部就包含了這么兩個注解。
@AutoConfigurationPackage //自動配置包 @Import(AutoConfigurationImportSelector.class)//自動配置導(dǎo)入選擇來看看@Import(AutoConfigurationImportSelector.class)中的內(nèi)容:
它幫我們導(dǎo)入了AutoConfigurationImportSelector,這個類中存在一個方法可以幫我們獲取所有的配置,代碼如下。
/* 所有的配置都存放在configurations中, 而這些配置都從getCandidateConfiguration中獲取, 這個方法是用來獲取候選的配置。 */ List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);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; }實際上它返回了一個List,這個List是由loadFactoryNames()方法返回的,其中傳入了一個getSpringFactoriesLoaderFactoryClass(),我們可以看看這個方法的內(nèi)容。
protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }我們看到了一個眼熟的詞 —— EnableAutoConfiguration,也就是說,它實際上返回的就是標(biāo)注了這個類的所有包。標(biāo)注了這個類的包不就是@SpringBootApplication嗎?
所以我們可以得出結(jié)論:它兜兜轉(zhuǎn)轉(zhuǎn)饒了這么多地方,就是為了將啟動類所需的所有資源導(dǎo)入。
我們接著往下看,它其中還有這么一條語句,是一條斷言:
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并且會去加載這個文件中的內(nèi)容呢?帶著這個疑問,我們首先找到spring.factories這個文件:
找到核心文件可以看到里面包含了很多自動配置屬性:
文件內(nèi)容我們可以隨便找一個自動配置點進去,比如WebMvcAutoConfiguration:
自動配置類這里放了所有關(guān)于WebMvc的配置,如視圖解析器、國際化等等。
分析到這里,我們就可以得出一個完整的結(jié)論了:
當(dāng)我們的SpringBoot項目啟動的時候,會先導(dǎo)入AutoConfigurationImportSelector,這個類會幫我們選擇所有候選的配置,我們需要導(dǎo)入的配置都是SpringBoot幫我們寫好的一個一個的配置類,那么這些配置類的位置,存在與META-INF/spring.factories文件中,通過這個文件,Spring可以找到這些配置類的位置,于是去加載其中的配置。
結(jié)論1流程看到這里,可能有些同學(xué)會存在疑問,spring.factories中存在那么多的配置,每次啟動時都是把它們?nèi)考虞d嗎?這顯然是不現(xiàn)實的。
這其實也是我在看源碼的時候存在疑問的地方,因為其中有一個注解并不常用,我們點開一個配置類就可以看到。
判斷注解@ConditionalOnXXX:如果其中的條件都滿足,該類才會生效。
所以在加載自動配置類的時候,并不是將spring.factories的配置全量加載進來,而是通過這個注解的判斷,如果注解中的類都存在,才會進行加載。
所以就實現(xiàn)了:我們在pom.xml文件中加入stater啟動器,SpringBoot自動進行配置。完成開箱即用。
-
-
結(jié)論
SpringBoot所有自動配置類都是在啟動的時候進行掃描并加載,通過spring.factories可以找到自動配置類的路徑,但是不是所有存在于spring,factories中的配置都進行加載,而是通過@ConditionalOnClass注解進行判斷條件是否成立(只要導(dǎo)入相應(yīng)的stater,條件就能成立),如果條件成立則加載配置類,否則不加載該配置類。
在這里貼一個我認為的比較容易理解的過程:
SpringBoot在啟動的時候從類路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值
將這些值作為自動配置類導(dǎo)入容器 , 自動配置類就生效 , 幫我們進行自動配置工作;
以前我們需要自己配置的東西 , 自動配置類都幫我們解決了
整個J2EE的整體解決方案和自動配置都在springboot-autoconfigure的jar包中;
它將所有需要導(dǎo)入的組件以全類名的方式返回 , 這些組件就會被添加到容器中 ;
它會給容器中導(dǎo)入非常多的自動配置類 (xxxAutoConfiguration), 就是給容器中導(dǎo)入這個場景需要的所有組件 , 并配置好這些組件 ;
-
有了自動配置類 , 免去了我們手動編寫配置注入功能組件等的工作;
最終結(jié)論流程
-
約定大于配置
開箱即用的原理說完了,約定大于配置就比較好理解了。其實約定大于配置就是開箱即用中那些自動配置的細節(jié)。說的具體點就是:我們的配置文件(.yml)應(yīng)該放在哪個目錄下,配置文件的命名規(guī)范,項目啟動時掃描的Bean,組件的默認配置是什么樣的(比如SpringMVC的視圖解析器)等等等等這一系列的東西,都可以被稱為約定,下面就來一點一點地說一下SpringBoot中的“約定”。
-
maven目錄結(jié)構(gòu)的約定
我們可以去Spring的官網(wǎng)查看一下官方文檔,看看文檔中描述的目錄結(jié)構(gòu)是怎樣的。
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/
這四個路徑從上到下存在優(yōu)先級關(guān)系。
-
SpringBoot默認配置文件的約定
SpringBoot默認可以加載一下三種配置文件:
- application.yml
- application.yaml
- application.properties
建議使用前兩種作為項目的配置文件。
-
項目啟動時掃描包范圍的約定
SpringBoot的注解掃描的默認規(guī)則是SpringBoot的入口類所在包及其子包。
若入口類所在的包是cn.objectspace.demo那么自動掃描包的范圍是cn.objectspace.demo包及其下面的子包,如果service包和dao包不在此范圍,則不會自動掃描。
SpringBoot自動配置類如何讀取yml配置
-
從更細節(jié)的角度去理解自動配置
上文中我們闡述了一些SpringBoot自動配置的原理,我們是從全局的角度去看自動配置的整個過程。比如從哪個地方開始進行裝配流程、如何找到裝配的包等。
那么現(xiàn)在將自己的視角貼近SpringBoot,來聊聊application.yml中我們配置的東西,是如何配置到一個個的配置類中的。
-
yml配置文件中可以配置那些東西
首先要知道這個問題的答案,我們應(yīng)該習(xí)慣springboot的配置方式。在上文中我們闡述了SpringBoot總是將所有的配置都用JavaConfig的形式去呈現(xiàn)出來,這樣能夠使代碼更加優(yōu)雅。那么yml中配置的東西,必然是要和這種配置模式去進行聯(lián)系的,我們在application.yml中配置的東西,通常是一些存在與自動配置類中的屬性,那么這些自動配置類,在啟動的時候是怎么找到的呢?如果你還記得上文的描述,那么你可以很明確地知道:spring.factories!沒錯,就是它,所以這個問題我們似乎得到了答案——只要存在與spring.factories中的,我們都可以在application.yml中進行配置。當(dāng)然,這并不意味著不存在其中的我們就不能配置,這些配置類我們是可以進行自定義的,只要我們寫了配置類,我們就可以在yml中配置我們需要的屬性值,然后在配置類中直接讀取這個配置文件,將其映射到配置類的屬性上。那么就牽扯出我們的問題了:配置類是如何去讀取yml配置文件中的信息的呢?
-
@ConfigurationProperties
要明白這個問題。我們就首先要去了解這個注解有什么作用。
我們可以自己嘗試在application.yml中去定義一些屬性,如下:
object: name: Object blogurl: blog.objectspace.cn我們現(xiàn)在自己定義一個類去讀取這個文件:
@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()); } }測試結(jié)果:
配置注入我們可以看到,在控制臺中輸出了我們在yml中配置的屬性值,但是這些值我們沒有在任何地方顯式地對這個對象進行注入。
所以@ConfigurationProperties這個注解,可以將yml文件中寫好的值注入到我們類的屬性中。
明白了它的作用,就能明白自動配置類工作的原理了。
我們依舊是選取SpringMVC的自動配置類,我們來看看其中有些什么東西。
mvcproperties點擊任意一個*Properties類中,look一下其中的內(nèi)容:
mvc配置注入看到這里相信所有人都明白了,我們就拿mvc配置來舉例。
yml演示我們在yml中配置的date-format,就可以通過@ConfigurationProperties映射到類中的dateFormat中,然后在通過自動配置類,將這些屬性配置到配置類中。
結(jié)語
歡迎大家訪問我的個人博客:Object's Blog















