深入淺出Spring Boot 起步依賴和自動配置

薛8發表於2019-05-11

深入淺出Spring Boot 起步依賴和自動配置

前言

我們知道 Spring Boot 能快速的搭建起一個應用,簡化了大量的配置過程,那到底有多"簡"呢?
我們通過一個例子來說明,平時我們通過 SpringSpring MVC 搭建一個helloword的 Web 應用,需要做以下工作:

    1. 配置 pom.xml 新增 SpringSpring MVC框架的依賴,同時還需要考慮這些不同的框架的不同版本是否存在不相容的問題。
    1. 配置 Web.xml,載入 Spring、Spring MVC。
    1. 配置 Spring 。
    1. 配置 Spring MVC。
    1. 編寫業務邏輯程式碼。

而使用 Spring Boot 搭建的話,需要做以下工作:

    1. 配置 pom.xml 繼承 Spring Boot 的 pom.xml,新增 Web 起步依賴。
    1. 建立啟動引導類。
    1. 編寫業務邏輯程式碼。

單從步驟數量上看就知道通過 Spring、Spring MVC 搭建比通過 Spring Boot 搭建更復雜,需要編寫大量的配置,這還僅僅是在很少框架和 Spring 整合情況下,如果需要將多個第三方框架和 Spring 整合,恐怕就要陷入"配置地獄"了,此外這些配置基本都是固化的,也就是搭建新的應用,你仍然需要再次編寫相同的配置資訊,特別是在微服務這麼火的當下,一個應用可能由十幾個甚至幾十個小型服務無組成,如果每個小型服務都重複的做著這些配置工作......。

那有沒有什麼辦法解決這個局面呢?答案是有的,那就是使用 Spring Boot,上從上面的例子就可以發現,使用 Spring Boot 的最大優點就是減少了配置的工作,那麼是不是說使用 Spring Boot 就不需要這些配置過程了?當然不是,而是 Spring Boot 幫我們把這些工作給做了。

那 Spring Boot 是如何幫我們把這些配置工作給做了呢?這就是本文需要探討的問題了,在探討之前,我們需要了解兩個概念起步依賴自動配置,這裡暫且知道這兩個東西是Spring Boot的核心、是Spring Boot的精華所在、是我們不需要再進行大量配置工作的原因所在就行了。

起步依賴

起步依賴說白了就是Spring Boot通過對常用的依賴進行再一次封裝,例如我們平時需要搭建一個Web應用的時候,一般都會匯入以下幾個依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.3.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.3.RELEASE</version>
</dependency>
複製程式碼

也就是需要將spring-webspring mvc分別匯入,而使用Spring Boot的話只需要匯入一個:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
複製程式碼

也就是隻需要匯入一個名為web的起步依賴即可,我們點spring-boot-starter-web進去可以看到,其實這個起步依賴整合了常用的 web 依賴,如下:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-json</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <version>2.1.4.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.0.16.Final</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.1.6.RELEASE</version>
  <scope>compile</scope>
</dependency>
複製程式碼

也就是前面所說的,Spring Boot的起步依賴說白了就是對常用的依賴進行再一次封裝,方便我們引入,簡化了 pom.xml 配置,但是更重要的是將依賴的管理交給了 Spring Boot,我們無需關注不同的依賴的不同版本是否存在衝突的問題,Spring Boot 都幫我們考慮好了,我們拿來用即可!

在使用 Spring Boot 的起步依賴之前,我們需要在pom.xml中新增配置:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
複製程式碼

即讓pom.xml繼承 Spring Boot 的pom.xml,而 Spring Boot 的pom.xml裡面定義了常用的框架的依賴以及相應的版本號。

總結一下 Spring Boot 的起步依賴的優點:

    1. 無需考慮不同框架的不同版本的衝突問題。
    1. 簡化了pom.xml配置。

自動配置

如果將開發一個應用比喻成裝修房子的過程,那麼 Spring Boot 就像是一個全能型公司一樣存在,而起步依賴可以比喻成購買裝修用品的過程,自動配置比喻成用裝修用品進行裝修的過程。

我們可以通過 Spring Boot 的起步依賴獲取到你想要的塗料、瓷磚、裝飾品等, Spring Boot 公司會根據最佳的組合將這些裝修用品打包好給我們,我們無需考慮各種裝修用品是否搭配、是否衝突等問題。

通過起步依賴我們獲取到了想要的裝修用品,那接下來我們需要做的就是進行裝修了,前面我們說過 Spring Boot 就像一個全能型公司一樣,所以我們在他那裡購買裝修用品之後,他不僅將裝修用品送上門還會幫我們完成裝修(自動配置),讓我們享受一站式的服務,從購買裝飾品(起步依賴)到裝修完成(自動配置)都不用我們考慮,我們只需要在裝修完成之後入住(編寫自己的業務邏輯程式碼)即可。

說了這麼多,我們還不知道Spring Boot是如何完成自動配置的,接下來我們來分析 Spring Boot 神奇的自動配置!

首先我們知道 Spring Boot 啟動需要有一個啟動引導類,這個類除了是應用的入口之外,還發揮著配置的 Spring Boot 的重要作用。下面是一個簡單的啟動引導類:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
複製程式碼

我們發現有一個名為@SpringBootApplication的註解,點選進去可以發現,這個註解發揮著多個註解的作用:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
複製程式碼

這裡簡要的說下@SpringBootConfiguration@ComponentScan註解。前者實質為@Configuration註解,這個註解相比大家都接觸過,也就是起到宣告這個類為配置類的作用,而後者起到開啟自動掃描元件的作用。

這裡需要重點分析的是@EnableAutoConfiguration這個註解,這個註解的作用是開啟 Spring Boot 的自動配置功能,我們來分析一下它是如何開啟的,點選進去可以看到:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}
複製程式碼

@EnableAutoConfiguration這個註解同樣發揮著多個註解的功能,我們重點分析@Import({AutoConfigurationImportSelector.class})這個註解,我們知道@import的作用是將元件新增到 Spring 容器中,而在這裡即是將AutoConfigurationImportSelector這個元件新增到 Spring 容器中。

我們進一步分析AutoConfigurationImportSelector

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); 
        configurations = this.removeDuplicates(configurations);
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.filter(configurations, autoConfigurationMetadata);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
複製程式碼

有一個名為getAutoConfigurationEntry的方法,這個方法發揮的作用是掃描ClassPath下的所有jar包的spring.factories檔案,將spring.factories檔案keyEnableAutoConfiguration的所有值取出,然後這些值其實是類的全限定名,也就是自動配置類的全限定名,然後 Spring Boot 通過這些全限定名進行類載入(反射),將這些自動配置類新增到 Spring 容器中。

那這些自動配置類有哪些?發揮什麼作用呢?我們接著往下看,我們找到一個名為spring-boot-autoconfigure-2.1.4.RELEASE.jar的 jar 包,開啟它的spring.factories檔案,發現這個檔案有keyEnableAutoConfiguration的鍵值對

深入淺出Spring Boot 起步依賴和自動配置

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\

......

複製程式碼

也就是這個jar包有自動配置類,可以發現這些自動配置配都是以xxxAutoConfiguration的命名規則來取名的,這些自動配置類包含我了們常用的框架的自動配置類,比如aopelasticsearchredisweb等等,基本能滿足我們日常開發的需求。

那這些自動配置類又是如何發揮配置作用的呢,我們取一個較為簡單的配置類進行分析,名為HttpEncodingAutoConfiguration,它的部分程式碼如下:

@Configuration  //宣告這個類為配置類
@EnableConfigurationProperties({HttpProperties.class})  //開啟ConfigurationProperties功能,同時將配置檔案和HttpProperties.class繫結起來
@ConditionalOnWebApplication(  //只有在web應用下自動配置類才生效
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class}) //只有存在CharacterEncodingFilter.class情況下 自動配置類才生效
@ConditionalOnProperty( //判斷配置檔案是否存在某個配置spring.http.encoding,如果存在其值為enabled才生效,如果不存在這個配置類也生效。
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)

//將字元編碼過濾器元件新增到 Spring 容器中
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
    return filter;
}

@Bean
public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
}
複製程式碼

首先它同樣有許多註解,我們一個一個分析:

  • Configuration:這個註解宣告瞭這個類為配置類(和我們平時寫的配置類一樣,同樣是在類上加這個註解)。
  • EnableConfigurationProperties:開啟ConfigurationProperties功能,也就是將配置檔案和HttpProperties.class這個類繫結起來,將配置檔案的相應的值和HttpProperties.class的變數關聯起來,可以點選HttpProperties.class進去看看,下面擷取了部分程式碼進行分析:
@ConfigurationProperties(
    prefix = "spring.http"
)

public static final Charset DEFAULT_CHARSET;
private Charset charset;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;
複製程式碼

通過ConfigurationProperties指定字首,將配置檔案application.properties字首為spring.http的值和HttpProperties.class的變數關聯起來,通過類的變數可以發現,我們可以設定的屬性是charsetforceforceRequestforceResponsemapping。也就是我們除了使用 Spring Boot 預設提供的配置資訊之外,我們還可以通過配置檔案指定配置資訊。

  • ConditionalOnWebApplication:這個註解的作用是自動配置類在 Web 應用中才生效。
  • ConditionalOnClass:只有在存在CharacterEncodingFilter這個類的情況下自動配置類才會生效。
  • ConditionalOnProperty:判斷配置檔案是否存在某個配置 spring.http.encoding ,如果存在其值為 enabled 才生效,如果不存在這個配置類也生效。

可以發現後面幾個註解都是ConditionalXXXX的命名規則,這些註解是 Spring 制定的條件註解,只有在符合條件的情況下自動配置類才會生效。

接下來的characterEncodingFilter方法,建立一個CharacterEncodingFilter的物件,也就是字元編碼過濾器,同時設定相關屬性,然後將物件返回,通過@Bean註解,將返回的物件新增到 Spring 容器中。這樣字元編碼過濾器元件配置好了,而平時的話,我們需要在 web.xml 進行如下配置:

  <filter>
       <filter-name>springUtf8Encoding</filter-name>
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
       <init-param>
           <param-name>encoding</param-name>
           <param-value>utf-8</param-value>
       </init-param>
       <init-param>
           <param-name>forceEncoding</param-name>
           <param-value>true</param-value>
       </init-param> 
    </filter>
    <filter-mapping>
       <filter-name>springUtf8Encoding</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>
複製程式碼

到這裡是不是感受到了 Spring Boot 自動配置帶來的好處了?

接下來的localeCharsetMappingsCustomizer方法同理,就不分析了。

最後我們用一句話總結一下 Spring Boot 的自動配置:Spring Boot 啟動的時候,會掃描ClassPath下的所有 jar 包,將其spring.factories檔案中keyEnableAutoConfiguration的所有值取出,然後這些值其實是類的全限定名,也就是自動配置類的全限定名,然後 Spring Boot 通過這些全限定名進行類載入(反射),將這些自動配置類新增到 Spring 容器中。這些自動配置類根據不同的條件(@ConditionalXXX)決定自動配置類是否生效,生效的話自動配置類會將相關元件新增到 Spring 容器中,也就不用我們再進行配置!

總結

看了網上挺多的文章都說的不是很清楚,所以按照自己的理解寫下了這篇總結,有誤之處歡迎指出。

原文地址:ddnd.cn/2019/05/10/…

相關文章