1、前言
從Spring轉到SpringBoot的xdm應該都有這個感受,以前整合Spring + MyBatis + SpringMVC我們需要寫一大堆的配置文件,堪稱配置文件地獄,我們還要在pom.xml文件里引入各種類型的jar包,Mybatis的、SpringMVC的、Spring-aop的,Spring-context等等。
自從使用SpringBoot后,新建一個項目幾乎不需要做任何改動,我們就可以運行起來。pom文件里,我們只需要引入一個spring-boot-starter-web
就可以,之前我們所做的一切,SpringBoot都在底層幫我們做了。
寫過SSM的xdm應該都記得dispatcherServle
t和characterEncoding
,這是我們在web.xml中必須配置的兩個選項,我們還需要配置文件上傳解析器multipartResolver
,需要配置數據源druidDataSource
,如果解析jsp,還需要配置視圖解析器viewResolver
。。。到最后,你就會有一坨的配置文件。
然后我們使用SpringBoot后,好像也從來沒有配置過這些東西了,以前閉着眼睛都能寫出來的各種配置文件,突然之間好像離我們很遙遠了,而我們也漸漸忘記了各種Filter(過濾器)和Interceptor(攔截器)的名字。這既是SpringBoot簡化了配置帶給我們的好處,也是它帶來的壞處。
這篇文章就一起來學習一下SpringBoot是如何做依賴管理以及自動配置的。
2、依賴管理
2.1 父項目做依賴管理
每個SpringBoot項目,pom.xml文件都會給我們定義一個parent節點
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
該節點指定了version
版本號,所以在pom.xml文件里我們很多引入的jar都沒有定義版本號,但這樣也不會出錯,因為SpringBoot幫我們為一些常用的jar包指定了版本號。
ctrl + 鼠標右鍵點擊進入spring-boot-starter-parent
這個jar包,會發現它的父項目是spring-boot-dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
而在這個jar包里,就聲明了很多開發中常用jar的版本號
所以在你pom.xml文件中引入jar的時候,如果該jar在spring-boot-dependencies
中定義了版本號,那么你可以不寫。如果你想使用其他的版本號,那么也可以在pom.xml中定義version,遵循就近原則。比如你想使用自定義版本號的MySQL驅動,只需在pom.xml中進行定義
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
2.2 starter場景啟動器
在SpringBoot項目中,我們只需要引入spring-boot-starter-web
包就可以寫接口並且進行訪問,因為在這個starter中整合了我們之前寫Spring項目時引入的spring-aop
、spring-context
、spring-webmvc
等jar包,包括tomcat,所以SpringBoot項目不需要外部的tomcat,只需要啟動application類使用內置的tomcat服務器即可。
在SpringBoot項目中,根據官方文檔,有各種場景的spring-boot-starter-*
可以使用,只要引入了starter,這個場景所有常規需要的依賴就會自動引入。(https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters)
所有場景啟動器最底層的依賴就是spring-boot-starter
,該jar包是核心啟動包,包含了自動配置的支持,日志以及YAML。Core starter, including auto-configuration support, logging and YAML,這是官方對它的描述。
而這個spring-boot-autoconfigure
就關系到我們接下來要說的SpringBoot自動配置功能。
3、容器功能
了解SpringBoot的自動配置功能之前,需要先了解一下SpringBoot的容器管理功能。學習Spring的時候就知道,Spring的IOC和AOP。
IOC容器幫助我們存放對象,並且管理對象,包括:創建、裝配、銷毀,這樣就將原本由程序完成的工作交給了Spring框架來完成。學習的核心在於如何將對象放在Spring中以及從Spring中取出。
3.1 SpringBoot的默認包掃描路徑
在SpringBoot中,我們沒有指定任何一個包的掃描路徑,但你注冊進容器中的對象卻都可以拿到,這是因為SpringBoot有默認的包掃描路徑,在這個路徑下的目標對象,都會被注冊進容器中。默認的掃描路徑是Main Application Class所在的目錄以及子目錄。可以通過scanBasePackages
屬性改變掃描路徑
@SpringBootApplication(scanBasePackages = "xxx.xxx.xxx")
該屬性其實和@ComponentScan
注解的basePackages
屬性綁定了,所以使用@ComponentScan
也能達到一樣的效果。
獲取默認掃描路徑在代碼在ComponentScanAnnotationParser
類的parse
方法中,在對應的行打上斷點,啟動主類進行調試
調試后就會發現,其實這個declaringClass
就是項目的啟動類,然后啟動類所在的包就會加入basePackages
中。
3.2 組件添加
(1)@Configuration和@Bean
@Configuration
注解表示這個類是個配置類,@Bean
注解往容器中注冊實例。
import com.codeliu.entity.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public User user() {
User user = new User("禿頭哥", 20);
return user;
}
}
然后在啟動類中進行測試,可以發現容器中的實例都是單例的,即多次拿到的都是同一個對象。
@SpringBootApplication
public class DockerTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DockerTestApplication.class, args);
User user1 = run.getBean("user", User.class);
User user2 = run.getBean(User.class);
// true
System.out.println(user1 == user2);
}
}
@Configuration
注解中的proxyBeanMethods
屬性即代理bean的方法,決定是否是單例模式,默認為true。Full模式(proxyBeanMethods = true)和Lite(proxyBeanMethods = false)模式,Full模式保證每個@Bean方法被調用多少次返回的組件都是單實例的,而Lite模式每個@Bean方法被調用多少次返回的組件都是新創建的。組件依賴必須使用Full模式默認,其他默認是否Lite模式
(2)@Component、@Controller、@Service、@Repository
四大法王,使用在pojo、mapper、service、controller類上的注解。
(3)@Import
該注解定義如下,只有一個value
屬性,你可以傳入一個Class數組,在啟動過程中,會自動幫你把類注冊進容器。
@Configuration
@Import({User.class, DBHelper.class})
public class MyConfig {
}
@SpringBootApplication
public class DockerTestApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DockerTestApplication.class, args);
User user1 = run.getBean(User.class);
User user2 = run.getBean(User.class);
// true
System.out.println(user1 == user2);
User user = run.getBean(User.class);
// com.codeliu.entity.User@63411512
System.out.println(user);
DBHelper dbHelper = run.getBean(DBHelper.class);
// ch.qos.logback.core.db.DBHelper@35cd68d4
System.out.println(dbHelper);
}
}
可以看到,默認組件的名字是全類名。
(4)@Conditional條件裝配
意思就是滿足@Conditional
指定的條件,才進行組件注入。
import com.codeliu.entity.User;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// 沒有名字為test的bean,才進行注冊,該注解可以放在類型,也可以放在方法上,作用范圍不一樣
@ConditionalOnMissingBean(name = "test")
public class MyConfig {
@Bean
public User user() {
User user = new User("禿頭哥", 20);
return user;
}
}
其他注解類似。在SpringBoot進行自動配置的時候,底層使用了很多條件裝配,達到按需加載的目的。
3.3 原生配置文件引入
@ImportResource
注解可以導入Spring的配置文件,讓配置文件里的內容生效。因為有些項目bean定義在xml文件里,但你必須知道xml文件的路徑,這樣在項目啟動的時候Spring才會加載配置文件。那對於SpringBoot項目來說,所有的bean都是通過java配置實現,xml沒有用武之地了嗎?
@Configuration
搭配@ImportResource
可以實現xml配置的裝載。
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource("classpath:beans.xml")
public class MyConfig {
}
3.4 配置綁定
很多時候我們需要讀取properties文件中的屬性,封裝到對應的Java bean中。我們可以通過代碼進行讀取
public class getProperties {
public static void main(String[] args) throws FileNotFoundException, IOException {
Properties pps = new Properties();
pps.load(new FileInputStream("a.properties"));
Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
while(enum1.hasMoreElements()) {
String strKey = (String) enum1.nextElement();
String strValue = pps.getProperty(strKey);
System.out.println(strKey + "=" + strValue);
//封裝到JavaBean。
}
}
}
當配置文件中屬性很多的時候,極其不方便。
(1)@Component和@ConfigurationProperties
在Java bean上使用這兩個注解,可以和配置文件中的屬性相關聯,不過要注意的是,Java bean必須有setter/getter方法,否則無法賦值,另外就是配置文件中的屬性不能有大寫字母,否則啟動報錯。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "my-user")
public class User {
private String names;
private int age;
....
}
省略了getter/setter和toString方法。@ConfigurationProperties
注解中指定配置文件中相關屬性的前綴,在配置文件中
my-user.names=CodeTiger
my-user.age=22
啟動類中進行測試獲取user對象,輸出就會發現屬性已經賦值。
(2)@ConfigurationProperties和@EnableConfigurationProperties
@ConfigurationProperties
注解加載java bean上指定前綴,而@EnableConfigurationProperties
注解則加在配置類上,該注解有兩個作用:開啟配置綁定功能、把指定的java bean注冊到容器中。因為該注解會把java bean注冊到容器中,所以在java bean上就不需要加@Component
注解了。
@Configuration
@EnableConfigurationProperties({User.class})
public class MyConfig {
}
4、自動配置原理
了解了上面的知識,就來學習一下SpringBoot底層是如何幫我們自動配置bean的。從加在啟動類上的@SpringBootApplication
注解開始。
上面四個注解跟我們本次學習無關,可以忽略。
(1)@SpringBootConfiguration
查看該注解的定義,發現其上標有@Configuration
,並且里面有一個唯一的屬性即proxyBeanMethods
。前面我們講@Configuration
注解的時候講過這個屬性,這里就不重復講了。這說明被@SpringBootConfiguration
修飾的類也是一個配置類。
(2)@ComponentScan
指定掃描哪些Spring注解。
(3)@EnableAutoConfiguration
這是SpringBoot自動配置的入口,該注解定義如下
4.1 自動配置basePackage
@AutoConfigurationPackage
注解,顧名思義,自動配置包。
@Import
注解前面講過,將一個組件注入容器中,所以我們看看AutoConfigurationPackages.Registrar
長啥樣子。它調用了register
方法進行組件的注冊,那么是注冊哪里的組件呢?看它第二個參數,是去獲取basePackage
,所以可以猜出@AutoConfigurationPackage
注解應該是在啟動項目的時候,自動把默認包或者我們指定的包路徑下面的組件注冊進容器。
我們進入PackageImports
類,在對應行上打上斷點進行調試,看看項目啟動后這些值是什么
可以發現,它會拿到啟動類所在的包路徑,然后返回給register
方法作為它的第二個參數傳入。這就是為什么在啟動時,我們不需要配置任何路徑,SpringBoot就可以幫我們把組件注入容器的原因。
4.2 自動配置導入包的配置類
這個是干什么的呢?前面我們說過,在Spring中我們會配置dispatcherServle
t和characterEncoding
等需要的組件,但在SpringBoot中,我們卻啥都沒做。
因為在SpringBoot底層幫我們做了。就是EnableAutoConfiguration
注解上標注的@Import(AutoConfigurationImportSelector.class)
注解。再貼一遍
所以就得去看看AutoConfigurationImportSelector
類了。
在AutoConfigurationImportSelector
類中有一個getAutoConfigurationEntry
方法,該方法就是給容器中批量導入一些組件。那么是導入哪些組件呢?在該方法中,拿到一個configurations
,然后對configurations
又是去重又是刪除。那獲取看看這個變量里面存的是個啥玩意就明白了。
在相應行打上斷點,運行后
這是啥?一個長度為130的數組,而里面都是一些AutoConfiguration,而且我們還看到了熟悉的AopAutoConfiguration。往下找找,發現還有我們熟悉的DispatcherServlet
那么這些自動配置類是從哪里讀取的呢?看方法里的getCandidateConfigurations
方法
該方法中調用loadFactoryNames
方法,而loadFactoryNames
方法則調用loadSpringFactories
方法, 利用工廠加載得到所有 META-INF/spring.factories
文件中的組件。
而 META-INF/spring.factories
文件存在於我們導入的jar包。它會掃描所有jar包中的 META-INF/spring.factories
文件,然后進行去重以及移除掉我們exclude掉的組件。
在我測試的項目中,獲取到的組件數目為130,就是在 spring-boot-autoconfigure-2.4.4.jar包中,里面剛好有130個組件。
到這里,總結一下大致的流程如下:
(1)利用getAutoConfigurationEntry(annotationMetadata);給容器中批量導入一些組件。
(2)調用List
(3)利用工廠加載 Map<String, List
(4)從META-INF/spring.factories位置來加載一個文件。默認掃描我們當前系統里面所有META-INF/spring.factories位置的文件。
4.3 按需開啟自動配置項
SpringBoot在啟動的時候為我們加載了這么多組件,我們不可能全部用得上,那如果用不上的還注冊進容器,豈不是耗費資源。其實底層使用了條件裝配@Conditional,在我們需要的情況下才會注冊對應的組件。
在我測試的項目中,因為啟動的時候都是加載的 spring-boot-autoconfigure-2.4.4.jar包中的組件,所以我可以去看看該jar包中的xxxAutoConfiguration的源碼。比如AopAutoConfiguration
在項目中,如果我們沒有引入aspectj的jar,就不會有Advice類,那么jdk動態代理和cglib代理都不會生效。而此時生效的是基礎代理,只作用於框架內部的advisors,項目中我們自定義的切面是不會被AOP代理的。
其他AutoConfiguration也是類似的,這里就不一一看了。
4.4 用戶優先
啥叫用戶優先?就是SpringBoot底層雖然會為我們自動加載組件,但如果我們想用我們自己定義的呢?來看看HttpEncodingAutoConfiguration
首先應用是Servlet應用以及存在CharacterEncodingFilter
類的時候,才會進行注冊。而且該類和配置文件進行了綁定,可以在配置文件中對屬性進行賦值。在注冊CharacterEncodingFilter
的時候,如果系統中不存在這個bean的時候,才會進行注冊,防止重復注冊,並且組件的值是進行動態賦值的,即如果我們編碼不想使用utf-8,那我們可以在配置文件中進行修改,系統注冊時候,就會使用我們自定義的值。
根據官方文檔,有以下屬性可以進行設置。
5、總結
本來主要分析了SpringBoot是如何進行依賴管理和自動配置的,相比於Spring,很多工作都是在底層幫我們做了。雖然我們寫代碼可能用不上這些,但知其然並且知其所以然,紙上得來終覺淺,絕知此事要躬行。