天天看點

SpringBoot自動裝配原了解析

本文包含:SpringBoot的自動配置原理及如何自定義SpringBootStar等

我們知道,在使用SpringBoot的時候,我們隻需要如下方式即可直接啟動一個Web程式:

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

和我們之前使用普通Spring時繁瑣的配置相比簡直不要太友善,那麼你知道SpringBoot實作這些的原理麼

首先我們看到類上方包含了一個

@SpringBootApplication

注解

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}           

這個注解上邊包含的東西還是比較多的,咱們先看一下兩個簡單的熱熱身

@ComponentScan

@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })           

這個注解咱們都是比較熟悉的,無非就是自動掃描并加載符合條件的Bean到容器中,這個注解會預設掃描聲明類所在的包開始掃描,例如:

cn.shiyujun.Demo

類上标注了

@ComponentScan

注解,則

cn.shiyujun.controller

cn.shiyujun.service

等等包下的類都可以被掃描到

這個注解一共包含以下幾個屬性:

basePackages:指定多個包名進行掃描
basePackageClasses:對指定的類和接口所屬的包進行掃
excludeFilters:指定不掃描的過濾器
includeFilters:指定掃描的過濾器
lazyInit:是否對注冊掃描的bean設定為懶加載
nameGenerator:為掃描到的bean自動命名
resourcePattern:控制可用于掃描的類檔案
scopedProxy:指定代理是否應該被掃描
scopeResolver:指定掃描bean的範圍
useDefaultFilters:是否開啟對@Component,@Repository,@Service,@Controller的類進行檢測           

@SpringBootConfiguration

這個注解更簡單了,它隻是對

Configuration

注解的一個封裝而已

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}           

EnableAutoConfiguration

這個注解可是重頭戲了,SpringBoot号稱的約定大于配置,也就是本文的重點自動裝配的原理就在這裡了

@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

    String[] excludeName() default {};
}           

簡單概括一下,這個注解存在的意義就是:利用

@Import

注解,将所有符合自動裝配條件的bean注入到IOC容器中,關于

@Import

注解原理這裡就不再闡述,感興趣的同學可以參考此篇文章:

Spring @Import注解源碼解析

進入類

AutoConfigurationImportSelector

,觀察其

selectImports

方法,這個方法執行完畢後,Spring會把這個方法傳回的類的全限定名數組裡的所有的類都注入到IOC容器中

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            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 StringUtils.toStringArray(configurations);
        }
    }           

觀察上方代碼:

  1. 第一行if時會首先判斷目前系統是否禁用了自動裝配的功能,判斷的代碼如下:
protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }           
  1. 如果目前系統禁用了自動裝配的功能則會傳回如下這個空的數組,後續也就無法注入bean了
private static final String[] NO_IMPORTS = new String[0];           
  1. 此時如果沒有禁用自動裝配則進入else分枝,第一步操作首先會去加載所有Spring預先定義的配置條件資訊,這些配置資訊在

    org.springframework.boot.autoconfigure

    包下的

    META-INF/spring-autoconfigure-metadata.properties

    檔案中
    1. 這些配置條件主要含義大緻是這樣的:如果你要自動裝配某個類的話,你覺得先存在哪些類或者哪些配置檔案等等條件,這些條件的判斷主要是利用了

      @ConditionalXXX

      注解,關于

      @ConditionalXXX

      系列注解可以參考這篇文章: SpringBoot條件注解@Conditional
      1. 這個檔案裡的内容格式是這樣的:
        org.springframework.boot.actuate.autoconfigure.web.servlet.WebMvcEndpointChildContextConfiguration.ConditionalOnClass=org.springframework.web.servlet.DispatcherServlet
        org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,io.micrometer.core.instrument.MeterRegistry
        org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration           
      2. 具體的加載代碼就不列出了,無法就是個讀取配置檔案
      3. 這裡放個加載之後的結果圖:
  2. 擷取

    @EnableAutoConfiguration

    注解上的exclude、excludeName屬性,這兩個屬性的作用都是排除一些類的
  3. 這裡又是關鍵的一步,可以看到剛才圖檔中spring-autoconfigure-metadata.properties檔案的上方存在一個檔案spring.factories,這個檔案可就不止存在于

    org.springframework.boot.autoconfigure

    包裡了,所有的包裡都有可能存在這個檔案,是以這一步是加載整個項目所有的spring.factories檔案。這個檔案的格式是這樣的
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthIndicatorAutoConfiguration,\org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration           
  1. 這裡存在一個知識點,SpringBoot中的star就是依靠這個檔案完成的,假如我們需要自定義一個SpringBoot的Star,就可以在我們的項目的META-INF檔案夾下建立一個spring.factories檔案
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.shiyujun.TestAutoConfiguration           

這樣當别的項目依賴我們的項目時就會自動把我們的

TestAutoConfiguration

類注入到Spring容器中

  1. 删除重複的自動配置類
  2. 下面三行就是去除我們指定排除的配置類
  3. 接着這一行的邏輯稍微複雜一些,主要就是根據加載的配置條件資訊來判斷各個配置類上的

    @ConditionalXXX

    系列注解是否滿足需求
  4. 最後就是釋出自動裝配完成事件,然後傳回所有能夠自動裝配的類的全限定名

到了這裡我們已經把SpringBoot自動裝配的原理搞清楚了,但是總感覺差點什麼,那我們從這些自動裝配的類裡面挑一個我們比較熟悉的關于Servlet的類來看看咋回事吧:

@Configuration
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
public class ServletEndpointManagementContextConfiguration {
    public ServletEndpointManagementContextConfiguration() {
    }

    @Bean
    public ExposeExcludePropertyEndpointFilter<ExposableServletEndpoint> servletExposeExcludePropertyEndpointFilter(WebEndpointProperties properties) {
        Exposure exposure = properties.getExposure();
        return new ExposeExcludePropertyEndpointFilter(ExposableServletEndpoint.class, exposure.getInclude(), exposure.getExclude(), new String[0]);
    }

    @Configuration
    @ConditionalOnClass({ResourceConfig.class})
    @ConditionalOnMissingClass({"org.springframework.web.servlet.DispatcherServlet"})
    public class JerseyServletEndpointManagementContextConfiguration {
        public JerseyServletEndpointManagementContextConfiguration() {
        }

        @Bean
        public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, ServletEndpointsSupplier servletEndpointsSupplier) {
            return new ServletEndpointRegistrar(properties.getBasePath(), servletEndpointsSupplier.getEndpoints());
        }
    }

    @Configuration
    @ConditionalOnClass({DispatcherServlet.class})
    public class WebMvcServletEndpointManagementContextConfiguration {
        private final ApplicationContext context;

        public WebMvcServletEndpointManagementContextConfiguration(ApplicationContext context) {
            this.context = context;
        }

        @Bean
        public ServletEndpointRegistrar servletEndpointRegistrar(WebEndpointProperties properties, ServletEndpointsSupplier servletEndpointsSupplier) {
            DispatcherServletPathProvider servletPathProvider = (DispatcherServletPathProvider)this.context.getBean(DispatcherServletPathProvider.class);
            String servletPath = servletPathProvider.getServletPath();
            if (servletPath.equals("/")) {
                servletPath = "";
            }

            return new ServletEndpointRegistrar(servletPath + properties.getBasePath(), servletEndpointsSupplier.getEndpoints());
        }
    }
}           

自上而下觀察整個類的代碼,你會發現這些自動裝配的套路都是一樣的

  1. 如果目前是Servlet環境則裝配這個bean
  2. 當存在類

    ResourceConfig

    以及不存在類

    DispatcherServlet

    時裝配

    JerseyServletEndpointManagementContextConfiguration

  3. 當存在

    DispatcherServlet

    類時裝配

    WebMvcServletEndpointManagementContextConfiguration

  4. 接下來如果還有面試官問你,你會了麼?