天天看點

深入剖析Spring Boot自動配置原理,核心概念及Tomcat自動啟動原理

作者:JAVA互聯搬磚勞工

導言

現在許多項目都廣泛采用了Spring Boot,你隻需要引入相應的starter,例如spring-boot-starter-web,然後啟動應用程式,就會自動啟動Tomcat Web伺服器并開始接收HTTP請求。那麼,這是如何實作的呢?它是如何知道要啟動Tomcat而不是Undertow? 另外,如果我希望使用Undertow嗎,要如何切換?本文将深入剖析背後的原理。

簡單的說,就是Spring Boot提供了一種自動配置(auto-configuration)機制:當項目引入一個包含自動配置的jar包時,根據特定的條件和規則,它會注冊不同的Bean到Spring容器中,進而啟動不同的功能特性。

那麼,具體什麼是自動配置,它是如何工作的?有哪些條件和規則?這些條件和規則又是如何比對和應用的?本文将分三個部分幫你全面了解自動配置的工作原理:

  • 核心概念:@AutoConfiguration(自動配置類)和@Conditional注解(條件比對)
  • 案例分析:Spring Boot是怎麼自動啟動Tomcat伺服器的?
  • 常見問題和FAQ
本文基于Spring Boot 3.0.x版本,同時也适用于Spring Boot 2.7.x版本。

核心概念 - 自動配置類@AutoConfiguration

什麼是自動配置類

使用過Spring架構的開發者應該對@Configuration注解非常熟悉了。在項目中,我們經常使用它來進行自定義的Bean配置。

而@AutoConfiguration是專門用于自動配置類的注解,而這些加了AutoConfiguration注解的自動配置類就是自動配置的入口。@AutoConfiguration本身也使用了@Configuration注解,表明自動配置類也是一個标準的配置類。

與标準的配置類相同,自動配置類的核心内容也是配置Bean,但是它會在此基礎上,添加各種條件和規則,隻有滿足特定的條件和規則,這些Bean才會生效。另外,這些條件規則也可以應用到自動配置類本身,控制整個自動配置類的開啟與否。

通常一個特定的自包含特性功能會對應一個自動配置類,但是配置本身不一定要都要全寫在這一個類裡,可以分解為多個普通的@Configuration配置類,然後通過@Import引入。

例如,Servlet Web伺服器相關功能的自動配置,入口就是一個自動配置類ServletWebServerFactoryAutoConfiguration,它将每個可選的Web伺服器配置都拆分到各自的@Configuration配置類中,部分代碼如下所示:

Java複制代碼@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class)  
@ConditionalOnWebApplication(type = Type.SERVLET)  
@EnableConfigurationProperties(ServerProperties.class)  
@Import({
ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}
           

我們來詳細分析下這個自動配置類上的注解。

  • @AutoConfiguration(after = SslAutoConfiguration.class):告訴Spring架構這個類是用于自動配置的。有些自動配置的初始化是有先後依賴關系,可以通過after,before來聲明這種依賴關系。
  • @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE):設定自動配置類加載的順序。
  • @ConditionalOnClass和@ConditionalOnWebApplication:這兩個注解就是自動配置生效的條件和規則,後面會詳細說明。
  • @EnableConfigurationProperties(ServerProperties.class):自動配置提供的自定義參數,比如server.port等。
  • @Import({...}):自動配置類一般是作為入口,簡單的配置可以直接寫在自動配置類裡。而複雜的配置建議按功能或範圍拆分成子配置,然後通過@Import引入。注意,引入的順序會影響條件的比對,尤其是選項類的配置(比如選擇Tomcat,Jetty還是Undertow)。

查找自動配置類

我們現在有了自動配置類,那麼Spring Boot是如何知道要加載這個自動配置類的呢?要知道,我們隻是單純引入了一個jar包而已,并沒有做任何設定。

答案是,Spring定義了一套自動配置專用的發現機制,就是jar包裡的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports檔案。該檔案的每一行就是一個自動配置類的完全限定名,比如下面是spring-boot-autoconfigure包裡該檔案的部分内容:

shell複制代碼## 其它自動配置類
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration  
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration  
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
## 其它自動配置類
           

可以看到ServletWebServerFactoryAutoConfiguration就在這個檔案裡。具體的加載邏輯可以檢視源碼AutoConfigurationImportSelector#getCandidateConfigurations。

imports檔案是Spring 2.7新引入的發現機制,之前版本使用的是spring.factories檔案。實際上,Spring 2.7.x版本兩種方式都支援,而在Spring 3.0中完全删除了對spring.factories檔案的相容支援。
【自動配置的子產品組織】一般簡單的自動配置子產品,隻有一個starter子產品。而複雜的配置,都會拆成兩個子產品:starter和autoconfigure。例如,Spring Boot将其内置的所有自動配置類都放在了spring-boot-autoconfigure包裡,包括imports檔案。而為每個單獨的功能特性提供了獨立的starter包,比如spring-boot-starter-web,spring-boot-starter-jdbc等。這些starter沒有任何Java代碼,唯一作用是引入所有需要的依賴。

核心概念 - 條件@Conditional

自動配置的核心是條件比對,不同的條件加載不同的Bean,進而啟用不同的功能特性。在Spring Boot中,使用了一系列的@ConditionalXXX注解來定義條件。其中,最常用的包括:

  • 類條件:@ConditionalOnClass和@ConditionalOnMissingClass,用于檢測類的存在與否。簡單的說就是,應用程式有沒有直接或者間接的引用了包含這個類的jar包。比如,你要開啟Undertow伺服器的自動配置,就要引入Undertow相關的jar包。
  • Bean條件:@ConditionalOnBean和@ConditionalOnMissingBean,用于檢測Spring容器中是否已經注冊了指定的Bean。通過使用這些條件注解,開發者可以根據需要注冊自定義的Bean,以覆寫預設的配置。比如Spring提供了多種DataSource,不過不包含Druid,你就可以自定義一個基于Druid的DataSource Bean,覆寫Spring預設提供的DataSource實作。
  • 屬性條件:@ConditionalOnProperty,用于檢測目前的Environment中是否配置了指定的屬性,這些屬性可以來自配置檔案,JVM系統屬性,作業系統的環境變量等。比如Hikari的其中一個條件是@ConfigurationProperties(prefix = "spring.datasource.hikari")。
  • 資源條件:@ConditionalOnResource,用于檢查是否存在特定的資源,比如是否存在某個配置檔案,這種條件用到的很少。
  • Web特定條件:@ConditionalOnWebApplication和@ConditionalOnNotWebApplication,用于檢測應用類型是否為Web應用。@ConditionalOnWarDeployment和@ConditionalOnNotWarDeployment注解用于判斷是否是一個部署在Servlet容器上的傳統WAR應用,而使用内嵌的web伺服器的應用就不符合此條件。
  • SpEL表達式條件:@ConditionalOnExpression可以用SpEL表達式指定條件規則。要注意, 如果在表達式中引入了其它bean,會導緻提早初始化這些bean。此時,這些Bean的狀态可能是不完整的,因為它還沒有經過Post Processor(比如屬性綁定)的處理。建議先用上面的幾種條件,無法滿足再考慮這種。

我們分析一個實際案例,ServletWebServerFactoryAutoConfiguration條件注解如下:

Java複制代碼@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class) // (1)
@ConditionalOnWebApplication(type = Type.SERVLET) // (2)
@EnableConfigurationProperties(ServerProperties.class)  
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,  
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}
           
  1. @ConditionalOnClass(ServletRequest.class):要求類路徑下必須存在ServletRequest類,這個很好了解,如果都沒用到Servlet相關的類和庫,說明你不需要Servlet Web服務相關的功能,也就沒必要啟動相關配置了。
  2. @ConditionalOnWebApplication(type = Type.SERVLET):隻是引入了Servlet相關類和庫,也不能表明這就是一個Servlet Web服務應用。這個條件就能確定目前啟動的應用是一個Servlet Web服務。

這兩個條件注解是應用在自動配置類上的,是一種總開關,如果不滿足,這個自動配置類就會被完全禁用。

如果滿足了類級别上的條件,就會繼續加載具體的配置,包括自動配置類裡定義的@Bean方法和@Import的配置類。

假設自動配置類的開關條件滿足了,我們看下Tomcat的具體配置,也就是@Import裡的ServletWebServerFactoryConfiguration.EmbeddedTomcat配置類,它的核心代碼如下:

Java複制代碼@Configuration(proxyBeanMethods = false) // (1)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) // (2) 
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // (3)
static class EmbeddedTomcat {
    @Bean // (4)
    TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {  
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();  
        // 其它初始化代碼
        return factory;  
    }
}
           

我們詳細分析下這個配置類:

  1. @Configuration(proxyBeanMethods = false):表明它是一個配置類,proxyBeanMethods=false表示這個配置類不需要用CGLIB增強@Bean方法,CGLIB增強後,可以以直接調用@Bean方法的方式,定義Bean之間的依賴關系。
  2. @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) :這些都是Tomcat的核心類。簡單的說,就是要求你引入Tomcat相關的jar包。同理,EmbeddedUndertow的條件就要求引入Jetty的核心類。
  3. @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) :字面意思是,隻有目前Spring容器中沒有ServletWebServerFactory類型的Bean,才會注冊這個Bean。換種說法就是,目前還沒有加載其它Web伺服器。其它可選的伺服器配置類,比如EmbeddedJetty和EmbeddedUndertow也是這個條件。你也可以注冊自定義的ServletWebServerFactory,覆寫Spring Boot自帶的Web伺服器。
  4. tomcatServletWebServerFactory:這個配置類隻有這一個@Bean方法,傳回的是一個工廠類Bean,它的作用是執行個體化,初始化一個Tomcat Web Server。隻有EmbeddedTomcat類上的條件注解都滿足之後,這個@Bean方法才會生效。

案例分析:Spring Boot是怎麼自動啟動Tomcat伺服器的?

上面講述了自動配置的基本原理和概念,接下來我們來回答文章開頭提出的問題:”我們隻是引入了spring-boot-starter-web包,Spring Boot是怎麼知道要自動啟動Tomcat伺服器的?具體是如何啟動的呢?”

第一個問題其實簡單,因為spring-boot-starter-web引入了spring-boot-starter-tomcat。

而關于其中的決策和啟動過程,上面講原理的時候其實已經提到了核心部分,無非就是條件比對,不過前面部分側重原理,知識點比較分散,這裡通過案例分析的方式,把整個過程串起來,再詳細說明下Spring Boot的整個決策過程。

第一步:掃描和注冊使用者自定義的Bean配置

這是所有Spring Boot啟動的标準步驟,這裡沒有什麼特殊的地方。隻需要知道自動配置類的解析和加載是在使用者自定義的Bean配置之後的。隻有這樣,自動配置才能根據使用者的自定義配置做調整。

第二步:查找自動配置類

在這一步,Spring會掃描類路徑下的所有jar包,查找自動配置類的注冊檔案META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,然後加載檔案裡的自動配置類。

我們的應用隻引入了spring-boot-starter-web包,但是這個包引入了spring-boot-starter,繼而引入了spring-boot-autoconfigure,我們可以從spring-boot-autoconfigure包下找到這個imports檔案,該檔案配置了Spring Boot内置的大量自動配置類,這裡我們隻關心Servlet Web伺服器相關的自動配置類org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration。

我們再回顧下這個類的源碼,後續會解析具體的條件比對過程。

Java複制代碼@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)  
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,  
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}
           

第三步:條件比對@ConditionalOnClass(ServletRequest.class)

spring-boot-starter-web包引入了spring-boot-starter-tomcat,繼而引入了tomcat-embed-core,這個包打包了JavaEE(Spring Boot 3.x之後是Jakarta EE)的類,其中就包含了ServletRequest類,這樣就滿足了該條件。

第四步:條件比對@ConditionalOnWebApplication(type = Type.SERVLET)

SpringApplication類的構造函數會調用下面這段代碼,判斷Web應用的類型。

Java複制代碼static WebApplicationType deduceFromClasspath() {  
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)  
    && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {  
        return WebApplicationType.REACTIVE;  
    }  
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {  
            return WebApplicationType.NONE;  
        }  
    }  
    return WebApplicationType.SERVLET;  
}
           

從這段代碼可以看出,目前應用是Servlet Web服務的前提,是存在相關的類SERVLET_INDICATOR_CLASSES,這個值在Spring Boot 3.x和之前的版本有些微差别,具體如下:

Java複制代碼// Spring Boot 3.x
String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",  
"org.springframework.web.context.ConfigurableWebApplicationContext" };

// Spring Boot 2.7.x
String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",  
"org.springframework.web.context.ConfigurableWebApplicationContext" };
           

差異就是Servlet類的包名改了,因為Spring 3.x從JavaEE更新到了Jakarta EE,Servlet跟第三步要求的ServletRequest類在同一個包下,是以這個條件也滿足了。剩下的就是org.springframework.web.context.ConfigurableWebApplicationContext類。從包名可以看出,它是spring-web中的一個類,我們分析下包的依賴關系,發現spring-web包是由spring-boot-starter-web包引入的。它其實是個接口,具體的實作類是ServletWebServerApplicationContext。

至此,ServletWebServerFactoryAutoConfiguration的兩個條件注解都滿足了。Spring Boot就會開始加載這個配置類以及它@Import的配置類。

第五步:加載@Import的EmbeddedTomcat

我們先看下EmbeddedTomcat的源碼:

Java複制代碼@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {  
        // ...
    }
}
           

spring-boot-starter-web引入了spring-boot-starter-tomcat,繼而引入了Tomcat相關的依賴包,是以滿足了第一個條件@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })。

由于我們隻是引入了spring-boot-starter-web包,沒有做任何配置,此時容器肯定沒有ServletWebServerFactory類型的Bean,是以滿足了第二個條件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)。

自此,EmbeddedTomcat配置的所有條件滿足,配置生效,@Bean方法tomcatServletWebServerFactory會被注冊到Spring容器中,在合适的階段用于建立TomcatServletWebServerFactory類型的Bean執行個體。

此外,剩下兩個被@Import的EmbeddedJetty和EmbeddedUndertow也還是會被處理的,但是由于我們沒有引入相應的Jetty或Undertow的包,是以條件都不滿足,它們的配置也就不會生效。其實,就算引入了需要的jar包,由于EmbeddedTomcat已經注冊了ServletWebServerFactory,這兩個配置類也不會生效,它們的源碼如下:

Java複制代碼@Configuration(proxyBeanMethods = false)  
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })  // 這些類都是Jetty核心包下的類
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已經注冊了,這個條件無法滿足
static class EmbeddedJetty {
    @Bean  
    JettyServletWebServerFactory jettyServletWebServerFactory(...) {  
        // ...
    }
}

@Configuration(proxyBeanMethods = false)  
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })  // 這些類都是Undertow核心包下的類
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已經注冊了,這個條件無法滿足
static class EmbeddedUndertow {
    @Bean  
    UndertowServletWebServerFactory undertowServletWebServerFactory(...) {
        // ...
    }
}
           

到目前位置,Web伺服器的選擇決策部分已經結束了,剩下的就是其它依賴Bean的配置,這裡就不再詳細展開了。

第六步:啟動内嵌的Tomcat伺服器

在容器初始化完畢後,會調用AbstractApplicationContext#onRefresh方法,而ServletWebServerApplicationContext會重寫該方法,在重寫的方法中調用createWebServer方法來建立一個WebServer執行個體。而具體要建立哪個WebServer執行個體,就是看容器中注冊的ServletWebServerFactory類型Bean。具體代碼如下:

scss複制代碼protected ServletWebServerFactory getWebServerFactory() {
    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }
           

從實際效果來看,就是調用了第五步注冊的tomcatServletWebServerFactory建立的工廠Bean,然後用這個工廠Bean建立了真正的Tomcat執行個體。

需要提一下,此時還隻是建立和初始化Tomcat執行個體,并沒有真正啟動服務。在SpringApplication啟動的最後一步,會觸發WebServerStartStopLifecycle的start()回調,這個回調觸發WebServer.start()方法,進而真正啟動一個Web伺服器,開始接收請求。

FAQ

1. 如何排除特定的自動配置類?

我們以排除自動資料源配置類為例,第一種方法是通過@SpringBootApplication的exclude字段:

Java複制代碼// 第一種方法,用exclude字段。
// 如果不想對類有依賴,可以用excludeName字段。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class)
public class Application {

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

第二種方法是在配置檔案中排除:

Java複制代碼spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
           

2. 不想用Tomcat,如何換成Undertow?

隻需要排除spring-boot-starter-tomcat,并引入spring-boot-starter-undertow。

xml複制代碼<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- 排除Tomcat的依賴 -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 替換成Undertow的依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
           

3. 如果同時直接或間接地引入了Tomcat,Jetty和Undertow的依賴包,最終啟動的是哪個?

實際測試發現,三個都引入的話,最終啟動的是Tomcat。而如果隻有Jetty和Undertow,實際啟動的是Jetty。沒有找到官方的優先級文檔,我猜測這跟@Import的順序有關,@Import就是按照Tomcat,Jetty和Undertow的順序引用的,Spring先看到了import的EmbeddedTomcat配置類,發現滿足條件,于是注冊了ServletWebServerFactory類型的Bean TomcatServletWebServerFactory,然後繼續檢查Jetty和Undertown,此時由于已經注冊了TomcatServletWebServerFactory,就不滿足條件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)了。

4. 某個配置類為什麼沒生效?要怎麼排查?

啟動應用的時候,加上-Ddebug參數,Spring就會列印出每個配置類的條件比對的細節。

作為案例,我們看下沒有exclude掉tomcat,同時又引入undertow的情況下,看看為什麼undertow沒有生效。從下面這個輸出可以看出,雖然比對了@ConditionalOnClass條件,但是沒有比對到@ConditionalOnMissingBean條件,具體原因是已經存在了tomcatServletWebServerFactory。

Java複制代碼ServletWebServerFactoryConfiguration.EmbeddedUndertow:
      Did not match:
         - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) found beans of type 'org.springframework.boot.web.servlet.server.ServletWebServerFactory' tomcatServletWebServerFactory (OnBeanCondition)
      Matched:
         - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'io.undertow.Undertow', 'org.xnio.SslClientAuthMode' (OnClassCondition)
           

總結

Spring Boot自動配置的核心思想就是将自動配置類和條件比對相結合,使得我們能夠快速內建各種功能群組件,而無需手動進行繁瑣的配置。

而從使用者的角度看,就是通過引入或者排除特定jar包的依賴,配置特定屬性和Bean,來影響條件的比對,進而靈活地配置和定制特定功能的開關和選項。

Spring Boot支援的所有自動配置類都配置在了spring-boot-autoconfigure包的META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports,引入一個新的Starter包的時候,強烈建議去看下相關的自動配置類。

作者:ByteWise

連結:https://juejin.cn/post/7260876695933124666

繼續閱讀