面試官:給我講講SpringBoot的依賴管理和自動配置?


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應該都記得dispatcherServlet和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-aopspring-contextspring-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中我們會配置dispatcherServlet和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 configurations = getCandidateConfigurations(annotationMetadata, attributes)獲取到所有需要導入到容器中的配置類。

(3)利用工廠加載 Map<String, List > loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的組件。

(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,很多工作都是在底層幫我們做了。雖然我們寫代碼可能用不上這些,但知其然並且知其所以然,紙上得來終覺淺,絕知此事要躬行。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM