聊聊 SpringBoot 自動裝配原理

本文已經收錄進 Github 95k+ Star 的Java項目JavaGuide 。JavaGuide項目地址 : github.com/Snailclimb/…前端

做者:Miki-byte-1024 & Snailclimbjava

每次問到 Spring Boot, 面試官很是喜歡問這個問題:「講述一下 SpringBoot 自動裝配原理?」。git

我以爲咱們能夠從如下幾個方面回答:github

  1. 什麼是 SpringBoot 自動裝配?
  2. SpringBoot 是如何實現自動裝配的?如何實現按需加載?
  3. 如何實現一個 Starter?

篇幅問題,這篇文章並無深刻,小夥伴們也能夠直接使用 debug 的方式去看看 SpringBoot 自動裝配部分的源代碼。web

前言

使用過 Spring 的小夥伴,必定有被 XML 配置統治的恐懼。即便 Spring 後面引入了基於註解的配置,咱們在開啓某些 Spring 特性或者引入第三方依賴的時候,仍是須要用 XML 或 Java 進行顯式配置。面試

舉個例子。沒有 Spring Boot 的時候,咱們寫一個 RestFul Web 服務,還首先須要進行以下配置。redis

@Configuration
public class RESTConfiguration {
    @Bean
    public View jsonTemplate() {
        MappingJackson2JsonView view = new MappingJackson2JsonView();
        view.setPrettyPrint(true);
        return view;
    }

    @Bean
    public ViewResolver viewResolver() {
        return new BeanNameViewResolver();
    }
}
複製代碼

spring-servlet.xmlspring

<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.xsd">

    <context:component-scan base-package="com.howtodoinjava.demo" />
    <mvc:annotation-driven />

    <!-- JSON Support -->
    <bean name="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
    <bean name="jsonTemplate" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>

</beans>
複製代碼

可是,Spring Boot 項目,咱們只須要添加相關依賴,無需配置,經過啓動下面的 main 方法便可。數據庫

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
複製代碼

而且,咱們經過 Spring Boot 的全局配置文件 application.propertiesapplication.yml便可對項目進行設置好比更換端口號,配置 JPA 屬性等等。json

爲何 Spring Boot 使用起來這麼酸爽呢? 這得益於其自動裝配。自動裝配能夠說是 Spring Boot 的核心,那究竟什麼是自動裝配呢?

什麼是 SpringBoot 自動裝配?

咱們如今提到自動裝配的時候,通常會和 Spring Boot 聯繫在一塊兒。可是,實際上 Spring Framework 早就實現了這個功能。Spring Boot 只是在其基礎上,經過 SPI 的方式,作了進一步優化。

SpringBoot 定義了一套接口規範,這套規範規定:SpringBoot 在啓動時會掃描外部引用 jar 包中的META-INF/spring.factories文件,將文件中配置的類型信息加載到 Spring 容器(此處涉及到 JVM 類加載機制與 Spring 的容器知識),並執行類中定義的各類操做。對於外部 jar 來講,只須要按照 SpringBoot 定義的標準,就能將本身的功能裝置進 SpringBoot。

沒有 Spring Boot 的狀況下,若是咱們須要引入第三方依賴,須要手動配置,很是麻煩。可是,Spring Boot 中,咱們直接引入一個 starter 便可。好比你想要在項目中使用 redis 的話,直接在項目中引入對應的 starter 便可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
複製代碼

引入 starter 以後,咱們經過少許註解和一些簡單的配置就能使用第三方組件提供的功能了。

在我看來,自動裝配能夠簡單理解爲:經過註解或者一些簡單的配置就能在 Spring Boot 的幫助下實現某塊功能。

SpringBoot 是如何實現自動裝配的?

咱們先看一下 SpringBoot 的核心註解 SpringBootApplication

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
<1.>@SpringBootConfiguration
<2.>@ComponentScan
<3.>@EnableAutoConfiguration
public @interface SpringBootApplication {

}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //實際上它也是一個配置類
public @interface SpringBootConfiguration {
}
複製代碼

大概能夠把 @SpringBootApplication看做是 @Configuration@EnableAutoConfiguration@ComponentScan 註解的集合。根據 SpringBoot 官網,這三個註解的做用分別是:

  • @EnableAutoConfiguration:啓用 SpringBoot 的自動配置機制

  • @Configuration:容許在上下文中註冊額外的 bean 或導入其餘配置類

  • @ComponentScan: 掃描被@Component (@Service,@Controller)註解的 bean,註解默認會掃描啓動類所在的包下全部的類 ,能夠自定義不掃描某些 bean。以下圖所示,容器中將排除TypeExcludeFilterAutoConfigurationExcludeFilter

@EnableAutoConfiguration 是實現自動裝配的重要註解,咱們以這個註解入手。

@EnableAutoConfiguration:實現自動裝配的核心註解

EnableAutoConfiguration 只是一個簡單地註解,自動裝配核心功能的實現實際是經過 AutoConfigurationImportSelector類。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //做用:將main包下的所欲組件註冊到容器中
@Import({AutoConfigurationImportSelector.class}) //加載自動裝配類 xxxAutoconfiguration
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

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

咱們如今重點分析下AutoConfigurationImportSelector 類到底作了什麼?

AutoConfigurationImportSelector:加載自動裝配類

AutoConfigurationImportSelector類的繼承體系以下:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}
複製代碼

能夠看出,AutoConfigurationImportSelector 類實現了 ImportSelector接口,也就實現了這個接口中的 selectImports方法,該方法主要用於獲取全部符合條件的類的全限定類名,這些類須要被加載到 IoC 容器中

private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // <1>.判斷自動裝配開關是否打開
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
          //<2>.獲取全部須要裝配的bean
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }
複製代碼

這裏咱們須要重點關注一下getAutoConfigurationEntry()方法,這個方法主要負責加載自動配置類的。

該方法調用鏈以下:

如今咱們結合getAutoConfigurationEntry()的源碼來詳細分析一下:

private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
        //<1>.
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            //<2>.
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            //<3>.
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            //<4>.
            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);
        }
    }
複製代碼

第 1 步:

判斷自動裝配開關是否打開。默認spring.boot.enableautoconfiguration=true,可在 application.propertiesapplication.yml 中設置

第 2 步

用於獲取EnableAutoConfiguration註解中的 excludeexcludeName

第 3 步

獲取須要自動裝配的全部配置類,讀取META-INF/spring.factories

spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
複製代碼

從下圖能夠看到這個文件的配置內容都被咱們讀取到了。XXXAutoConfiguration的做用就是按需加載組件。

不光是這個依賴下的META-INF/spring.factories被讀取到,全部 Spring Boot Starter 下的META-INF/spring.factories都會被讀取到。

因此,你能夠清楚滴看到, druid 數據庫鏈接池的 Spring Boot Starter 就建立了META-INF/spring.factories文件。

若是,咱們本身要建立一個 Spring Boot Starter,這一步是必不可少的。

第 4 步

到這裏可能面試官會問你:「spring.factories中這麼多配置,每次啓動都要所有加載麼?」。

很明顯,這是不現實的。咱們 debug 到後面你會發現,configurations 的值變小了。

由於,這一步有經歷了一遍篩選,@ConditionalOnXXX 中的全部條件都知足,該類纔會生效。

@Configuration
// 檢查相關的類:RabbitTemplate 和 Channel是否存在
// 存在纔會加載
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
}
複製代碼

有興趣的童鞋能夠詳細瞭解下 Spring Boot 提供的條件註解

  • @ConditionalOnBean:當容器裏有指定 Bean 的條件下
  • @ConditionalOnMissingBean:當容器裏沒有指定 Bean 的狀況下
  • @ConditionalOnSingleCandidate:當指定 Bean 在容器中只有一個,或者雖然有多個可是指定首選 Bean
  • @ConditionalOnClass:當類路徑下有指定類的條件下
  • @ConditionalOnMissingClass:當類路徑下沒有指定類的條件下
  • @ConditionalOnProperty:指定的屬性是否有指定的值
  • @ConditionalOnResource:類路徑是否有指定的值
  • @ConditionalOnExpression:基於 SpEL 表達式做爲判斷條件
  • @ConditionalOnJava:基於 Java 版本做爲判斷條件
  • @ConditionalOnJndi:在 JNDI 存在的條件下差在指定的位置
  • @ConditionalOnNotWebApplication:當前項目不是 Web 項目的條件下
  • @ConditionalOnWebApplication:當前項目是 Web 項 目的條件下

如何實現一個 Starter

光說不練假把式,如今就來擼一個 starter,實現自定義線程池

第一步,建立threadpool-spring-boot-starter工程

第二步,引入 Spring Boot 相關依賴

第三步,建立ThreadPoolAutoConfiguration

第四步,在threadpool-spring-boot-starter工程的 resources 包下建立META-INF/spring.factories文件

最後新建工程引入threadpool-spring-boot-starter

測試經過!!!

總結

Spring Boot 經過@EnableAutoConfiguration開啓自動裝配,經過 SpringFactoriesLoader 最終加載META-INF/spring.factories中的自動配置類實現自動裝配,自動配置類其實就是經過@Conditional按需加載的配置類,想要其生效必須引入spring-boot-starter-xxx包實現起步依賴

文章結尾

我是 Guide 哥,一 Java 後端開發,會一點前端,自由的少年。咱們下期再見!微信搜「JavaGuide」回覆「面試突擊」領取我整理的 4 本原創PDF

相關文章
相關標籤/搜索