SpringBoot自動裝配原理解析

  • 2019 年 10 月 6 日
  • 筆記

本文包含: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.controllercn.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文件中
  2. 這些配置條件主要含義大致是這樣的:如果你要自動裝配某個類的話,你覺得先存在哪些類或者哪些配置文件等等條件,這些條件的判斷主要是利用了@ConditionalXXX註解,關於@ConditionalXXX系列註解可以參考這篇文章:SpringBoot條件註解@Conditional
  3. 這個文件里的內容格式是這樣的:
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  
  1. 具體的載入程式碼就不列出了,無法就是個讀取配置文件
  2. 這裡放個載入之後的結果圖:
  1. 獲取@EnableAutoConfiguration註解上的exclude、excludeName屬性,這兩個屬性的作用都是排除一些類的
  2. 這裡又是關鍵的一步,可以看到剛才圖片中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

這裡存在一個知識點,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. 接下來如果還有面試官問你,你會了么?