芋道 Spring Boot 自動配置原理

轉載自  芋道 Spring Boot 自動配置原理

1. 概述

友情提示:因爲本文是分享 Spring Boot 自動配置的原理,所以需要胖友有使用過 Spring Boot 的經驗。如果還沒使用過的胖友,不用慌,先跳轉到《芋道 Spring Boot SpringMVC 入門》文章,將前兩節閱讀完,感受下 Spring Boot 的魅力。

Spring Boot 自動配置,顧名思義,是希望能夠自動配置,將我們從配置的苦海中解脫出來。那麼既然要自動配置,它需要解三個問題:

  • 滿足什麼樣的條件

  • 創建哪些 Bean?

  • 創建的 Bean 的屬性

我們來舉個示例,對照下這三個問題。在我們引入 spring-boot-starter-web 依賴,會創建一個 8080 端口的內嵌 Tomcat,同時可以通過 application.yaml 配置文件中的 server.port 配置項自定義端口。那麼這三個問題的答案如下:

友情提示:爲了更易懂,這裏的答案暫時是表象的,不絕對精準。

  • 滿足什麼樣的條件?因爲我們引入了 spring-boot-starter-web 依賴。

  • 創建哪些 Bean?創建了一個內嵌的 Tomcat Bean,並進行啓動。

  • 創建的 Bean 的屬性?通過 application.yaml 配置文件的 server.port 配置項,定義 Tomcat Bean 的啓動端口屬性,並且默認值爲 8080。

壯着膽子,我們來看看 Spring Boot 提供的 EmbeddedWebServerFactoryCustomizerAutoConfiguration 類,負責創建內嵌的 Tomcat、Jetty 等等 Web 服務器的配置類。代碼如下:

@Configuration // <1.1>
@ConditionalOnWebApplication // <2.1>
@EnableConfigurationProperties(ServerProperties.class) // <3.1>
public class  EmbeddedWebServerFactoryCustomizerAutoConfiguration {

 /**
  * Nested configuration if Tomcat is being used.
  */
 @Configuration // <1.2>
 @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
 public static class TomcatWebServerFactoryCustomizerConfiguration {

  @Bean
  public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(
    Environment environment, ServerProperties serverProperties) {
   // <3.2>
   return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
  }

 }

 /**
  * Nested configuration if Jetty is being used.
  */
 @Configuration // <1.3>
 @ConditionalOnClass({ Server.class, Loader.class, WebAppContext.class })
 public static class JettyWebServerFactoryCustomizerConfiguration {

  @Bean
  public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(
    Environment environment, ServerProperties serverProperties) {
    // <3.3>
   return new JettyWebServerFactoryCustomizer(environment, serverProperties);
  }

 }

 /**
  * Nested configuration if Undertow is being used.
  */
 // ... 省略 UndertowWebServerFactoryCustomizerConfiguration 代碼

 /**
  * Nested configuration if Netty is being used.
  */
 // ... 省略 NettyWebServerFactoryCustomizerConfiguration 代碼

}

在開始看代碼之前,我們先來簡單科普下 Spring JavaConfig 的小知識。在 Spring3.0 開始,Spring 提供了 JavaConfig 的方式,允許我們使用 Java 代碼的方式,進行 Spring Bean 的創建。示例代碼如下:

@Configuration
public class DemoConfiguration {

    @Bean
    public void object() {
        return new Obejct();
    }

}
  • 通過在上添加 @Configuration 註解,聲明這是一個 Spring 配置類。

  • 通過在方法上添加 @Bean 註解,聲明該方法創建一個 Spring Bean。

OK,現在我們在回過頭看看 EmbeddedWebServerFactoryCustomizerAutoConfiguration 的代碼,我們分成三塊內容來講,剛好解決我們上面說的三個問題:

  • ① 配置類

  • ② 條件註解

  • ③ 配置屬性

① 配置類

<1.1> 處,在類上添加了 @Configuration 註解,聲明這是一個配置類。因爲它的目的是自動配置,所以類名以 AutoConfiguration 作爲後綴。

<1.2><1.3> 處,分別是用於初始化 Tomcat、Jetty 相關 Bean 的配置類。

  • TomcatWebServerFactoryCustomizerConfiguration 配置類,負責創建 TomcatWebServerFactoryCustomizer Bean,從而初始化內嵌的 Tomcat 並進行啓動。

  • JettyWebServerFactoryCustomizer 配置類,負責創建 JettyWebServerFactoryCustomizer Bean,從而初始化內嵌的 Jetty 並進行啓動。

如此,我們可以得到結論一,通過 @Configuration 註解的配置類,可以解決“創建哪些 Bean”的問題。

實際上,Spring Boot 的 spring-boot-autoconfigure 項目,提供了大量框架的自動配置類,稍後我們在「2. 自動配置類」小節詳細展開。

② 條件註解

<2> 處,在類上添加了 @ConditionalOnWebApplication 條件註解,表示當前配置類需要在當前項目是 Web 項目的條件下,才能生效。在 Spring Boot 項目中,會將項目類型分成 Web 項目(使用 SpringMVC 或者 WebFlux)和非 Web 項目。這樣我們就很容易理解,爲什麼 EmbeddedWebServerFactoryCustomizerAutoConfiguration 配置類會要求在項目類型是 Web 項目,只有 Web 項目纔有必要創建內嵌的 Web 服務器呀。

<2.1><2.2> 處,在類上添加了 @ConditionalOnClass 條件註解,表示當前配置類需要在當前項目有指定類的條件下,才能生效。

  • TomcatWebServerFactoryCustomizerConfiguration 配置類,需要有 tomcat-embed-core 依賴提供的 Tomcat、UpgradeProtocol 依賴類,才能創建內嵌的 Tomcat 服務器。

  • JettyWebServerFactoryCustomizer 配置類,需要有 jetty-server 依賴提供的 Server、Loader、WebAppContext 類,才能創建內嵌的 Jetty 服務器。

如此,我們可以得到結論二,通過條件註解,可以解決“滿足什麼樣的條件?”的問題。

實際上,Spring Boot 的 condition 包下,提供了大量的條件註解,稍後我們在「2. 條件註解」小節詳細展開。

③ 配置屬性

<3.1> 處,使用 @EnableConfigurationProperties 註解,讓 ServerProperties 配置屬性類生效。在 Spring Boot 定義了 @ConfigurationProperties 註解,用於聲明配置屬性類,將指定前綴的配置項批量注入到該類中。例如 ServerProperties 代碼如下:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties
  implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered {

 /**
  * Server HTTP port.
  */
 private Integer port;

 /**
  * Context path of the application.
  */
 private String contextPath;
 
 // ... 省略其它屬性
 
}
  • 通過 @ConfigurationProperties 註解,聲明將 server 前綴的配置項,設置到 ServerProperties 配置屬性類中。

<3.2><3.3> 處,在創建 TomcatWebServerFactoryCustomizer 和 JettyWebServerFactoryCustomizer 對象時,都會將 ServerProperties 傳入其中,作爲後續創建的 Web 服務器的配置。也就是說,我們通過修改在配置文件的配置項,就可以自定義 Web 服務器的配置。

如此,我們可以得到結論三,通過配置屬性,可以解決“創建的 Bean 的屬性?”的問題。


🐶 至此,我們已經比較清晰的理解 Spring Boot 是怎麼解決我們上面提出的三個問題,但是這樣還是無法實現自動配置。例如說,我們引入的 spring-boot-starter-web 等依賴,Spring Boot 是怎麼知道要掃碼哪些配置類的。下面,繼續我們的旅途,繼續抽絲剝繭。

2. 自動配置類

在 Spring Boot 的 spring-boot-autoconfigure 項目,提供了大量框架的自動配置,如下圖所示:

在我們通過 SpringApplication#run(Class<?> primarySource, String... args) 方法,啓動 Spring Boot 應用的時候,有個非常重要的組件 SpringFactoriesLoader 類,會讀取 META-INF 目錄下的 spring.factories 文件,獲得每個框架定義的需要自動配置的配置類

我們以 spring-boot-autoconfigure 項目的 Spring Boot spring.factories 文件來舉個例子,如下圖所示:

如此,原先 @Configuration 註解的配置類,就升級成類自動配置類。這樣,Spring Boot 在獲取到需要自動配置的配置類後,就可以自動創建相應的 Bean,完成自動配置的功能。

旁白君:這裏其實還有一個非常有意思的話題,作爲拓展知識,胖友可以後續去看看。實際上,我們可以把 spring.factories 理解成 Spring Boot 自己的 SPI 機制。感興趣的胖友,可以看看如下的文章:

  • 《Spring Boot 的 SPI 機制》

  • 《Java 的 SPI 機制》

  • 《Dubbo 的 SPI 機制》

實際上,自動配置只是 Spring Boot 基於 spring.factories 的一個拓展點 EnableAutoConfiguration。我們從上圖中,還可以看到如下的拓展點:

  • ApplicationContextInitializer

  • ApplicationListener

  • AutoConfigurationImportListener

  • AutoConfigurationImportFilter

  • FailureAnalyzer

  • TemplateAvailabilityProvider

因爲 spring-boot-autoconfigure 項目提供的是它選擇的主流框架的自動配置,所以其它框架需要自己實現。例如說,Dubbo 通過 dubbo-spring-boot-project 項目,提供 Dubbo 的自動配置。如下圖所示:

3. 條件註解

條件註解並不是 Spring Boot 所獨有,而是在 Spring3.1 版本時,爲了滿足不同環境註冊不同的 Bean ,引入了 @Profile 註解。示例代碼如下:

@Configuration
public class DataSourceConfiguration {

    @Bean
    @Profile("DEV")
    public DataSource devDataSource() {
        // ... 單機 MySQL
    }

    @Bean
    @Profile("PROD")
    public DataSource prodDataSource() {
        // ... 集羣 MySQL
    }
    
}
  • 在測試環境下,我們註冊單機 MySQL 的 DataSource Bean。

  • 在生產環境下,我們註冊集羣 MySQL 的 DataSource Bean。

在 Spring4 版本時,提供了 @Conditional 註解,用於聲明在配置類或者創建 Bean 的方法上,表示需要滿足指定條件才能生效。示例代碼如下:

@Configuration
public class TestConfiguration {

    @Bean
    @Conditional(XXXCondition.class)
    public Object xxxObject() {
        return new Object();
    }
    
}
  • 其中,XXXCondition 需要我們自己實現 Condition 接口,提供具體的條件實現。

顯然,Spring4 提交的 @Conditional 註解非常不方便,需要我們自己去拓展。因此,Spring Boot 進一步增強,提供了常用的條件註解:

  • @ConditionalOnBean:當容器裏有指定 Bean 的條件下

  • @ConditionalOnMissingBean:當容器裏沒有指定 Bean 的情況下

  • @ConditionalOnSingleCandidate:當指定 Bean 在容器中只有一個,或者雖然有多個但是指定首選 Bean

  • @ConditionalOnClass:當類路徑下有指定類的條件下

  • @ConditionalOnMissingClass:當類路徑下沒有指定類的條件下

  • @ConditionalOnProperty:指定的屬性是否有指定的值

  • @ConditionalOnResource:類路徑是否有指定的值

  • @ConditionalOnExpression:基於 SpEL 表達式作爲判斷條件

  • @ConditionalOnJava:基於 Java 版本作爲判斷條件

  • @ConditionalOnJndi:在 JNDI 存在的條件下差在指定的位置

  • @ConditionalOnNotWebApplication:當前項目不是 Web 項目的條件下

  • @ConditionalOnWebApplication:當前項目是 Web項 目的條件下

4. 配置屬性

Spring Boot 約定讀取 application.yamlapplication.properties 等配置文件,從而實現創建 Bean 的自定義屬性配置,甚至可以搭配 @ConditionalOnProperty 註解來取消 Bean 的創建。

咳咳咳,貌似這個小節沒有太多可以分享的內容,更多胖友可以閱讀《芋道 Spring Boot 配置文件入門》文章。

5. 內置 Starter

我們在使用 Spring Boot 時,並不會直接引入 spring-boot-autoconfigure 依賴,而是使用 Spring Boot 內置提供的 Starter 依賴。例如說,我們想要使用 SpringMVC 時,引入的是 spring-boot-starter-web 依賴。這是爲什麼呢?

因爲 Spring Boot 提供的自動配置類,基本都有 @ConditionalOnClass 條件註解,判斷我們項目中存在指定的類,纔會創建對應的 Bean。而擁有指定類的前提,一般是需要我們引入對應框架的依賴。

因此,在我們引入 spring-boot-starter-web 依賴時,它會幫我們自動引入相關依賴,從而保證自動配置類能夠生效,創建對應的 Bean。如下圖所示:

Spring Boot 內置了非常多的 Starter,方便我們引入不同框架,並實現自動配置。如下圖所示:

6. 自定義 Starter

在一些場景下,我們需要自己實現自定義 Starter 來達到自動配置的目的。例如說:

  • 三方框架並沒有提供 Starter,比如說 Swagger、XXL-JOB 等。

  • Spring Boot 內置的 Starter 無法滿足自己的需求,比如說 spring-boot-starter-jdbc 不提供多數據源的配置。

  • 隨着項目越來越大,想要提供適合自己團隊的 Starter 來方便配置項目,比如說永輝彩食鮮 csx-bsf-all 項目。

下面,我們一起來實現一個自定義 Starter,實現一個 Java 內置 HttpServer 服務器的自動化配置。最終項目如下圖所示:

在開始示例之前,我們要了解下 Spring Boot Starter 的命名規則,顯得我們更加專業(裝逼)。命名規則如下:

場景 命名規則 示例
Spring Boot 內置 Starter spring-boot-starter-{框架} spring-boot-starter-web
框架 自定義 Starter {框架}-spring-boot-starter mybatis-spring-boot-starter
公司 自定義 Starter {公司}-spring-boot-starter-{框架} 暫無,艿艿自己的想法哈

6.1 yunai-server-spring-boot-starter 項目

創建 yunai-server-spring-boot-starter 項目,實現一個 Java 內置 HttpServer 服務器的自動化配置。考慮到示例比較簡單,我們就不像 Spring Boot 拆分成 spring-boot-autoconfigure 和 spring-boot-starter-{框架} 兩個項目。

6.1.1 引入依賴

在 pom.xml 文件中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>lab-47</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>yunai-server-spring-boot-starter</artifactId>

    <dependencies>
        <!-- 引入 Spring Boot Starter 基礎庫 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
    </dependencies>

</project>

6.1.2 YunaiServerProperties

在 cn.iocoder.springboot.lab47.yunaiserver.autoconfigure 包下,創建 YunaiServerProperties 配置屬性類,讀取 yunai.server 前綴的配置項。代碼如下:

@ConfigurationProperties(prefix = "yunai.server")
public class YunaiServerProperties {

    /**
     * 默認端口
     */
    private static final Integer DEFAULT_PORT = 8000;

    /**
     * 端口
     */
    private Integer port = DEFAULT_PORT;

    public static Integer getDefaultPort() {
        return DEFAULT_PORT;
    }

    public Integer getPort() {
        return port;
    }

    public YunaiServerProperties setPort(Integer port) {
        this.port = port;
        return this;
    }

}

6.1.3 YunaiServerAutoConfiguration

在 cn.iocoder.springboot.lab47.yunaiserver.autoconfigure 包下,創建 YunaiServerAutoConfiguration 自動配置類,在項目中存在 com.sun.net.httpserver.HttpServer 類時,創建 HttpServer Bean,並啓動該服務器。代碼如下:

@Configuration // 聲明配置類
@EnableConfigurationProperties(YunaiServerProperties.class) // 使 YunaiServerProperties 配置屬性類生效
public class YunaiServerAutoConfiguration {

    private Logger logger = LoggerFactory.getLogger(YunaiServerAutoConfiguration.class);

    @Bean // 聲明創建 Bean
    @ConditionalOnClass(HttpServer.class) // 需要項目中存在 com.sun.net.httpserver.HttpServer 類。該類爲 JDK 自帶,所以一定成立。
    public HttpServer httpServer(YunaiServerProperties serverProperties) throws IOException {
        // 創建 HttpServer 對象,並啓動
        HttpServer server = HttpServer.create(new InetSocketAddress(serverProperties.getPort()), 0);
        server.start();
        logger.info("[httpServer][啓動服務器成功,端口爲:{}]", serverProperties.getPort());

        // 返回
        return server;
    }

}
  • 代碼比較簡單,胖友看看艿艿在代碼上添加的註釋喲。

6.1.4 spring.factories

在 resources 目錄下創建,創建 META-INF 目錄,然後在該目錄下創建 spring.factories 文件,添加自動化配置類爲 YunaiServerAutoConfiguration。內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.springboot.lab47.yunaiserver.autoconfigure.YunaiServerAutoConfiguration

至此,我們已經完成了一個自定義的 Starter。下面,我們在「6.2 lab-47-demo 項目」中引入,然後進行測試。

6.2 lab-47-demo 項目

創建 lab-47-demo 項目,引入我們自定義 Starter。

6.2.1 引入依賴

在 pom.xml 文件中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>lab-47</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-47-demo</artifactId>

    <dependencies>
        <!-- 引入自定義 Starter -->
        <dependency>
            <groupId>cn.iocoder.springboot.labs</groupId>
            <artifactId>yunai-server-spring-boot-starter</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

6.2.2 配置文件

在 resource 目錄下,創建 application.yaml 配置文件,設置 yunai.server.port 配置項來自定義 HttpServer 端口。配置如下:

yunai:
  server:
    port: 8888 # 自定義 HttpServer 端口

6.2.3 DemoApplication

創建 DemoApplication.java 類,配置 @SpringBootApplication 註解即可。代碼如下:

@SpringBootApplication
public class DemoApplication {

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

}

6.2.4 簡單測試

執行 DemoApplication#main(String[] args) 方法,啓動 Spring Boot 應用。打印日誌如下:

2020-02-02 13:03:12.156  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : Starting DemoApplication on MacBook-Pro-8 with PID 76469 (/Users/yunai/Java/SpringBoot-Labs/lab-47/lab-47-demo/target/classes started by yunai in /Users/yunai/Java/SpringBoot-Labs)
2020-02-02 13:03:12.158  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2020-02-02 13:03:12.873  INFO 76469 --- [           main] c.i.s.l.y.a.YunaiServerAutoConfiguration : [httpServer][啓動服務器成功,端口爲:8888]
2020-02-02 13:03:12.927  INFO 76469 --- [           main] c.i.s.lab47.demo.DemoApplication         : Started DemoApplication in 1.053 seconds (JVM running for 1.47)
  • YunaiServerAutoConfiguration 成功自動配置 HttpServer Bean,並啓動該服務器在 8888 端口。

此時,我們使用瀏覽器訪問 http://127.0.0.1:8888/ 地址,返回結果爲 404 Not Found。因爲我們沒有給 HttpServer 相應的 Handler。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章