Java 後端底層 ch08 Spring Boot 啟動流程:自動組態的魔法怎麼運作
CH 08 Spring

Spring Boot 啟動流程:自動組態的魔法怎麼運作

從 main() 到能接請求自動組態的載入與過濾@ConditionalOnMissingBean 的讓位機制設定優先順序啟動慢與 CrashLoopBackOff寫一個 starter

這是這一軌的最後一章,也是把前面七章串起來的地方。

Spring Boot 最大的賣點是「約定優於配置」——引入一個 spring-boot-starter-data-jpa,什麼都不用設定,DataSource、EntityManagerFactory、交易管理器就全部有了。

這很方便,但代價是當它不如預期時,你完全不知道要從哪裡找起:

① 引入了 starter,但那個 Bean 就是沒有被建立
② 自己定義了一個 DataSource,結果 Spring 的預設設定全部消失了
③ 本機讀到的設定值是 A,容器裡跑出來卻是 B
④ 本機啟動 8 秒,上了 Kubernetes 一直 CrashLoopBackOff

這四種狀況沒有一個會給你清楚的錯誤訊息。要能診斷它們,就得知道啟動的那幾秒鐘裡到底發生了什麼。

這一章的路徑:

  • 從 main() 到能接請求的完整階段,以及每個階段可以插手的擴充點。
  • 自動組態的運作方式:先載入一整份清單,再靠 @Conditional 過濾——理解這個「先全載入再過濾」的順序,前面第 ①② 種狀況就能自己推理。
  • @ConditionalOnMissingBean 的讓位機制:為什麼你自己寫一個 Bean 就能覆蓋官方的預設。
  • 設定的優先順序:12 層 PropertySource 的完整順序,以及容器環境變數如何無聲地覆蓋你的設定檔。
  • 啟動慢的診斷,以及它在 Kubernetes 上如何演變成永遠起不來。
  • 自己寫一個 starter——把前面所有機制串成一個可運作的東西。

這章會用到 ch06 的 Bean 生命週期(BeanFactoryPostProcessor 與 BeanPostProcessor 的差別在這裡變得很重要),以及 ch03 的連線池預熱。最後一張卡會把八章的主線收攏成一句話。

一行 SpringApplication.run(App.class, args) 背後大約做了八件事。先看骨架,後面再挑重點展開。

① 建立 SpringApplication 物件
   推斷應用類型(Servlet/Reactive/None——靠 classpath 有沒有某些類別)
   從 spring.factories 載入 ApplicationContextInitializer 與 ApplicationListener

② 準備 Environment
   載入 application.yml、環境變數、命令列參數 → 組成 PropertySource 清單
   ★ EnvironmentPostProcessor 在這裡插手(設定值還沒被任何人讀走)

③ 印出 Banner

④ 建立 ApplicationContext
   依應用類型選擇 AnnotationConfigServletWebServerApplicationContext 等

⑤ 準備 Context
   執行 ApplicationContextInitializer
   註冊主類別的 BeanDefinition(★ 此時還沒有任何 Bean 實例)

⑥ refresh()  ← 整個啟動的核心
   invokeBeanFactoryPostProcessors()   ★ 掃描套件、處理自動組態、產生所有 BeanDefinition
   registerBeanPostProcessors()
   finishBeanFactoryInitialization()   ★ 實例化所有單例 Bean(ch06 的生命週期在這裡跑)
   onRefresh()                          ★ 啟動內嵌 Tomcat

⑦ 執行 ApplicationRunner / CommandLineRunner

⑧ 發布 ApplicationReadyEvent —— 可以接請求了

兩個要記住的分界

分界一(第 ⑥ 步的前半 vs 後半):BeanDefinition 全部產生完,才開始實例化。
這代表自動組態的「決定要不要建立某個 Bean」,發生在任何 Bean 被實例化之前——所以它能根據「使用者有沒有定義某個 Bean」來決定讓不讓位。

分界二:內嵌 Tomcat 在所有單例 Bean 建立完之後才啟動。
這是刻意的——避免服務還沒準備好就開始接請求。也因此,任何 Bean 初始化很慢,都會直接變成「啟動慢」。

Spring Boot 相對於 Spring 多做了什麼

  • 自動組態——根據 classpath 上有什麼,自動決定要建立哪些 Bean
  • 內嵌容器——Tomcat/Jetty/Undertow 變成一個普通的 Bean,不再需要外部部署
  • starter 依賴聚合——一個依賴帶進一整組相容的版本
  • Externalized Configuration——統一的多來源設定機制
💡 「Spring Boot 是 Spring 的加強版」這個說法容易誤導。它沒有改變 Spring 容器本身——第 ⑥ 步的 refresh() 完全是 Spring 原本就有的東西。Boot 做的是「在 refresh 之前,幫你把 BeanDefinition 準備好」。

從註解拆起

@SpringBootApplication
  = @SpringBootConfiguration    (就是 @Configuration)
  + @ComponentScan              (掃描主類別所在套件及其子套件)
  + @EnableAutoConfiguration    ★ 魔法在這裡

@EnableAutoConfiguration 做的事

@EnableAutoConfiguration
  → @Import(AutoConfigurationImportSelector.class)
    → 讀取所有 jar 裡的設定清單檔:

      Spring Boot 2.7 以前:
        META-INF/spring.factories
      Spring Boot 2.7+/3.x:
        META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

    → 拿到一份「所有候選自動組態類別」的清單(通常一兩百個)
關鍵在這裡:這份清單是先全部載入的,不管你有沒有用到。
然後才逐一套用 @Conditional 判斷「這一個到底要不要生效」。

所以自動組態的本質是:一份很長的候選清單 + 一組過濾條件。

過濾是怎麼進行的

DataSourceAutoConfiguration
  @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
    → classpath 上有沒有這些類別?沒有就整個跳過

  內部的 @Bean 方法:
  @ConditionalOnMissingBean(DataSource.class)
    → 容器裡「還沒有」DataSource 才建立

為什麼引入 starter 就會生效

把因果鏈接起來:

  1. 引入 spring-boot-starter-data-jpa
  2. 它把 HikariCP、Hibernate 等 jar 帶進 classpath
  3. @ConditionalOnClass 的條件成立
  4. 對應的自動組態生效,`DataSource`、`EntityManagerFactory` 等 Bean 被建立
✅ 所以「引入依賴就自動生效」不是魔法,是「classpath 上有什麼」這個條件被滿足了。反過來說,某個 Bean 沒被建立時,第一件要檢查的事就是「那個 jar 真的在 classpath 上嗎」。

效能上的取捨

一兩百個候選類別都要被載入並評估條件,這是 Spring Boot 啟動比純 Spring 慢的主因之一。Spring Boot 用了兩個優化:

  • 自動組態順序(@AutoConfigureBefore/After/Order)避免重複評估
  • spring-autoconfigure-metadata.properties——把條件的中繼資料預先建好索引,讓明顯不成立的候選可以快速被排除,不用真的去載入類別

「我自己定義一個 DataSource,Spring 的預設就不會生效」——這件事幾乎每個人都知道,但它為什麼能成立?

時序才是關鍵

refresh() → invokeBeanFactoryPostProcessors()

  ① ConfigurationClassPostProcessor 開始工作
  ② 先處理 @ComponentScan —— 掃描「你自己的」類別
     → 你的 @Configuration、@Bean、@Service 全部註冊成 BeanDefinition
  ③ 最後才處理 @Import 進來的自動組態類別 ★
     → 此時評估 @ConditionalOnMissingBean
     → 發現「已經有一個 DataSource 的 BeanDefinition 了」→ 放棄建立
自動組態之所以能「讓位」,是因為它被刻意安排在最後處理。使用者定義的 Bean 先進入容器,自動組態才來檢查「還缺什麼」。

這也解釋了為什麼判斷依據是 BeanDefinition 而不是 Bean 實例——這個階段一個 Bean 都還沒被實例化(第一張卡的分界一)。

常用的 @Conditional 家族

  • @ConditionalOnClass / @ConditionalOnMissingClass——classpath 上有沒有某個類別
  • @ConditionalOnBean / @ConditionalOnMissingBean——容器裡有沒有某個 Bean
  • @ConditionalOnProperty——設定檔裡某個屬性的值(最常用來做開關)
  • @ConditionalOnWebApplication / @ConditionalOnNotWebApplication
  • @ConditionalOnResource——某個檔案存不存在
  • @ConditionalOnExpression——SpEL 表達式(最靈活,但也最難讀)

一個經常踩到的陷阱

@ConditionalOnBean 對「順序」非常敏感。
它判斷的是「此刻容器裡有沒有」——如果被依賴的那個 Bean 還沒被註冊,條件就不成立,而且之後也不會重新評估。

徵狀:加了一個新的自動組態之後,另一個原本正常的 Bean 突然消失了;或者在本機正常、換個環境就失效。
修法:用 @AutoConfigureAfter(XxxAutoConfiguration.class) 明確宣告順序。在自己寫的自動組態裡,盡量避免用 @ConditionalOnBean,改用 @ConditionalOnClass 或 @ConditionalOnProperty。

怎麼看到條件評估的結果

java -jar app.jar --debug

=========================
CONDITIONS EVALUATION REPORT
=========================

Positive matches:      ← 生效了的
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes

Negative matches:      ← 沒生效的,+原因
   RedisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class
           'org.springframework.data.redis.core.RedisOperations'
💡 「某個東西沒生效」時,這份報告是第一個該看的地方——它會直接告訴你是哪個條件沒過。比在網路上搜尋錯誤訊息有效得多。

啟動流程的每個階段都留了插手的地方。選錯階段是很常見的問題——因為在錯的階段做對的事,結果就是沒有效果。

① EnvironmentPostProcessor —— 改設定值

執行時機:Environment 準備好之後,Context 建立之前
用途:動態加入 PropertySource,例如從設定中心拉設定
註冊:必須寫在 spring.factories/.imports 裡(此時容器還不存在,不能用 @Component)

② ApplicationContextInitializer —— 改容器本身

執行時機:Context 建立之後、refresh() 之前
用途:對 ApplicationContext 做設定(例如註冊額外的 PropertySource、設定 profile)
註冊:同樣要寫在 spring.factories/.imports

③ BeanFactoryPostProcessor —— 改「設計圖」

執行時機:所有 BeanDefinition 註冊完,Bean 實例化之前
用途:修改 BeanDefinition,甚至動態註冊新的 Bean

★ 自動組態本身就是靠 ConfigurationClassPostProcessor(一種 BFPP)實作的

④ BeanPostProcessor —— 改「實例」

執行時機:每個 Bean 初始化前後(ch06 生命週期的第 ⑤⑦ 步)
用途:包裝或替換 Bean 實例

★ AOP 代理就是靠這個做的(ch07)
③ 和 ④ 的差別是整個 Spring 擴充機制裡最重要的一組對比:
• BeanFactoryPostProcessor 改的是「設計圖」(BeanDefinition)——此時還沒有物件,可以改類別、改作用域、改建構參數,甚至決定「不要建立這個 Bean」
• BeanPostProcessor 改的是「成品」(Bean 實例)——物件已經存在,只能包裝、替換或修改屬性

要影響「要不要建立、建立成什麼樣」用 BFPP;要影響「建立出來之後做什麼」用 BPP。

⑤ ApplicationRunner / CommandLineRunner —— 啟動後執行

執行時機:所有 Bean 就緒、內嵌容器已啟動之後
用途:資料初始化、暖機、一次性任務
差別:ApplicationRunner 拿到解析過的 ApplicationArguments
      CommandLineRunner 拿到原始的 String[]
順序:用 @Order 控制

另外:事件監聽

  • ApplicationReadyEvent——完全就緒(要做暖機請用這個,不要用 @PostConstruct)
  • ApplicationFailedEvent——啟動失敗,適合做告警
  • ContextClosedEvent——優雅關機時的清理
🚨 不要在 @PostConstruct 裡做外部呼叫(打 API、連 MQ、預熱快取)。那個階段容器還沒準備好、內嵌容器還沒啟動,一旦外部服務慢或掛掉,整個應用會卡在啟動階段而不是「啟動成功但功能降級」——後者顯然好得多。這類工作屬於 ApplicationRunner 或 ApplicationReadyEvent。

Spring Boot 會從十幾個地方讀設定,然後依優先順序疊起來——高優先的覆蓋低優先的。這是「本機正常、容器裡不對」最常見的原因。

優先順序(由高到低,簡化版)

① 命令列參數                      --server.port=9090
② SPRING_APPLICATION_JSON         整包 JSON 塞進來
③ ServletConfig / ServletContext 參數
④ JNDI
⑤ System.getProperties()          -Dserver.port=9090
⑥ 作業系統環境變數                 SERVER_PORT=9090        ★ 容器最常用
⑦ 打包外的 application-{profile}.yml
⑧ 打包內的 application-{profile}.yml
⑨ 打包外的 application.yml
⑩ 打包內的 application.yml        ★ 你平常改的那個
⑪ @PropertySource
⑫ SpringApplication.setDefaultProperties()
典型事故:環境變數無聲覆蓋設定檔。
Kubernetes 的 Deployment 裡設了 SPRING_DATASOURCE_URL,而你改了 application.yml 裡的 spring.datasource.url 卻完全沒有作用——因為環境變數(第 ⑥ 層)優先於設定檔(第 ⑩ 層)。

而且完全不會有任何警告,你只會看到它連到了「不知道哪來的」資料庫。

Relaxed Binding:名稱的對應規則

spring.datasource.url   ←→   SPRING_DATASOURCE_URL
                                 大寫、點換成底線

my-app.max-retry-count  ←→   MYAPP_MAXRETRYCOUNT ?  ❌ 錯
                        ←→   MY_APP_MAX_RETRY_COUNT   ✅ 對
                             (連字號也變成底線)

怎麼確認實際生效的值

# Actuator(正式環境記得保護好這個端點)
/actuator/env                      # 列出所有 PropertySource 與各自的值
/actuator/env/spring.datasource.url  # 單一屬性:看它從哪一層來、被誰覆蓋
/actuator/configprops              # 所有 @ConfigurationProperties 的實際值

Profile 的常見誤用

# ❌ 以為會「合併」
application.yml:            spring.datasource.hikari.maximum-pool-size: 10
application-prod.yml:       spring.datasource.hikari.connection-timeout: 3000

# 對於 map/list 型別,高優先的 PropertySource 會「整個取代」而不是逐項合併
# 純量屬性則是逐項覆蓋(上面這個例子其實兩個都會生效)
💡 規則:純量屬性逐項覆蓋,集合型(List/Map)整個取代。踩到最多的是 List——在 profile 檔裡只想「加一項」,結果整個蓋掉了原本的清單。

先量,不要猜

# 方法一:啟動時的階段耗時
java -jar app.jar --debug

# 方法二:Spring Boot 2.4+ 的啟動追蹤(最精確)
@Bean
ApplicationStartup applicationStartup() {
    return new BufferingApplicationStartup(2048);
}
# 然後看 /actuator/startup —— 會列出每個步驟的耗時

# 方法三:看哪些 Bean 建立得慢
--debug 的 log 裡搜尋 "Bean creation" 的時間戳

五個常見原因

  1. @ComponentScan 範圍過大——直接掃 com 之類的根套件,掃描與條件評估的時間暴增。維持 Spring Boot 的預設(主類別所在套件)。
  2. 大量無用的自動組態——引入了一堆用不到的 starter,每個都要載入並評估條件。用 --debug 看 Positive matches 裡有沒有你根本沒在用的。
  3. 連線池預熱(ch03)——minimum-idle 設得高時,啟動要先建立這些連線;如果資料庫在遠端,每條連線幾十毫秒就會累積起來。
  4. Flyway/Liquibase 遷移——每次啟動都要檢查、必要時執行 DDL。
  5. @PostConstruct 裡做外部呼叫——這是最糟的一種,見下面。

Kubernetes 上的連鎖反應

徵狀:本機啟動 8 秒完全正常,上了 k8s 卻不斷 CrashLoopBackOff,log 看起來很正常、只是還沒印完就被砍掉。

成因:
① 容器的 CPU limit 讓 JIT 編譯與類別載入變慢,啟動時間從 8 秒變成 25 秒
② livenessProbe 的 initialDelaySeconds 設 10 秒
③ 10 秒時應用還沒起來 → 探測失敗 → k8s 認為它掛了,重啟
④ 重啟後再跑一次 → 再次失敗 → 永遠起不來

更糟的是:這時候 log 裡沒有任何錯誤,因為應用根本沒有錯——只是不夠快。

修法

# ① 用 startupProbe 把「啟動」和「存活」分開(k8s 1.18+,推薦)
startupProbe:
  httpGet: { path: /actuator/health/readiness, port: 8080 }
  failureThreshold: 30      # 最多容忍 30 × 5 = 150 秒
  periodSeconds: 5
livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8080 }
  periodSeconds: 10         # startupProbe 通過後才開始生效

# ② 給足 CPU(啟動期比穩定期需要更多 CPU)
resources:
  requests: { cpu: "1" }    # 不要設成 100m

# ③ 分開 readiness 與 liveness 端點
management.endpoint.health.probes.enabled=true
💡 這跟 ch04 的 Full GC 停頓導致 liveness 失敗是同一類問題:健康檢查沒有區分「還沒好」與「壞掉了」。startupProbe 就是為了解決這個而生的。

寫一次 starter 是驗證前面理解的最好方式——因為它會用到自動組態、條件註解、設定綁定與載入順序。

命名慣例

  • 官方:spring-boot-starter-xxx
  • 第三方:xxx-spring-boot-starter(不要佔用官方前綴)

① 設定屬性類別

@ConfigurationProperties(prefix = "audit")
public class AuditProperties {
    private boolean enabled = true;
    private String appName = "unknown";
    private Duration timeout = Duration.ofSeconds(3);
    // getter / setter
}

② 自動組態類別

@AutoConfiguration
@EnableConfigurationProperties(AuditProperties.class)
@ConditionalOnClass(AuditService.class)
@ConditionalOnProperty(prefix = "audit", name = "enabled",
                       havingValue = "true", matchIfMissing = true)
public class AuditAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean          // ★ 讓使用者能覆蓋
    public AuditService auditService(AuditProperties props) {
        return new DefaultAuditService(props);
    }
}

③ 註冊(這一步最容易漏)

Spring Boot 3.x:
  src/main/resources/META-INF/spring/
    org.springframework.boot.autoconfigure.AutoConfiguration.imports

  內容就是一行類別全名:
  com.example.audit.AuditAutoConfiguration

Spring Boot 2.7 以前:
  src/main/resources/META-INF/spring.factories
  org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.example.audit.AuditAutoConfiguration
最常見的失敗:自動組態類別沒被載入。
原因幾乎都是這兩個:
① 忘了寫 .imports 或 spring.factories——自動組態類別不會被 @ComponentScan 掃到,因為它通常不在使用者的套件底下
② Spring Boot 3 還在用舊的 spring.factories——3.x 已經不再讀取 EnableAutoConfiguration 那個 key

診斷:用 --debug 看 CONDITIONS EVALUATION REPORT,如果你的類別連在 Negative matches 裡都找不到,代表它根本沒被載入——問題出在註冊,不在條件。

④ 三個讓 starter 好用的細節

  • 永遠加 @ConditionalOnMissingBean——讓使用者能覆蓋你的預設,這是 starter 的基本禮貌
  • 提供 enabled 開關(@ConditionalOnProperty 配 matchIfMissing = true)——預設啟用但隨時能關掉
  • 加上 spring-boot-configuration-processor——編譯時產生中繼資料,讓使用者在 IDE 裡有自動完成與說明

自動組態的代價

Spring Boot 讓你不用寫設定,代價是「不知道自己在用什麼」。這在順利時完全不是問題,出事時卻是最大的障礙。

  • 不要害怕 --debug——條件評估報告是這套魔法的說明書
  • 不要為了「看起來乾淨」過度抽 starter——內部專案抽出五六個 starter 之後,沒有人知道一個 Bean 到底從哪來
  • 不要在 @PostConstruct 做外部呼叫——把「啟動失敗」變成「啟動成功但功能降級」,永遠是比較好的選擇
  • 知道怎麼關掉它:@SpringBootApplication(exclude = XxxAutoConfiguration.class)

這八章其實只講了三件事

一、每一層都有一個「看不見的成本」,效能問題幾乎都是它造成的。
• ch01 磁碟 I/O ——所以要用 B+Tree 壓低樹高
• ch03 建立連線 ——所以要有池
• ch04 STW 停頓 ——所以 GC 要分代
• ch05 主記憶體存取 ——所以 CPU 有快取,於是有了可見性問題

先找出成本在哪,設計的理由就會自己浮現。
二、最危險的 bug 是不會報錯的那種。
• ch01 隱式型別轉換讓索引失效——SQL 合法、結果正確,只是慢幾百倍
• ch02 長事務——沒有慢查詢、CPU 不高,只是磁碟慢慢滿
• ch04 exit code 137——沒有例外、沒有 heap dump
• ch06 循環依賴時的 AOP 失效——@Transactional 靜默失效
• ch07 自我呼叫、checked exception 不回滾

所以每一章都花很多篇幅在「徵狀」上——因為找得到病徵,才有機會診斷。
三、先找根因,再調旋鈕。
• ch03:先看 SQL → 再看交易邊界 → 最後才調連線池
• ch04:先看 OOM 類型 → 再看是不是洩漏 → 最後才調 GC 參數
• ch06/ch07:先問「這個設計合理嗎」→ 再考慮 @Lazy 或自我注入

倒過來做的話,通常會在事故報告上寫下「已將參數由 20 調整為 100」,然後下週再出一次事。
✅ 這一軌到此結束。如果只能帶走一句話,那就是——當某個東西「沒有按照預期運作」時,先問「它的運作機制是什麼」,而不是先找「怎麼讓它動起來的偏方」。前七章的每一個陷阱,都是這句話的練習題。
啟動階段與可插手的擴充點
階段發生什麼擴充點適合做什麼
② 準備 Environment組成 PropertySource 清單EnvironmentPostProcessor從設定中心拉設定;此時設定值還沒被任何人讀走
⑤ 準備 ContextContext 建好、尚未 refreshApplicationContextInitializer註冊額外 PropertySource、設定 profile
⑥ refresh 前半產生所有 BeanDefinition(還沒有實例)BeanFactoryPostProcessor改「設計圖」:改類別/作用域/建構參數,或決定不要建立
⑥ refresh 後半實例化所有單例 BeanBeanPostProcessor改「成品」:包裝或替換實例——AOP 代理就在這裡(ch07)
⑦ 啟動完成後Bean 就緒、內嵌容器已啟動ApplicationRunner / ApplicationReadyEvent資料初始化、暖機、一次性任務——不要放在 @PostConstruct
設定優先順序(高 → 低)
順位來源範例備註
① 最高命令列參數--server.port=9090臨時覆蓋最方便
⑤System Properties-Dserver.port=9090JVM 層級
⑥作業系統環境變數SERVER_PORT=9090容器/k8s 最常用;會無聲覆蓋設定檔,事故重災區
⑦⑧application-{profile}.ymlapplication-prod.yml打包外優先於打包內
⑨⑩application.yml—你平常改的那個,優先序其實很低
⑫ 最低setDefaultProperties()—程式碼寫死的預設值
常見狀況診斷表
狀況第一個該看的地方常見原因
某個 Bean 沒被建立--debug 的 CONDITIONS EVALUATION REPORT 的 Negative matchesjar 不在 classpath(@ConditionalOnClass 沒過)、被 @ConditionalOnMissingBean 讓位
自訂的自動組態完全沒作用報告裡連 Negative matches 都找不到它忘了寫 .imports/spring.factories;或 Boot 3 還在用舊的 spring.factories
設定值跟預期不同/actuator/env/{屬性名} 看它從哪一層來環境變數(第 ⑥ 層)覆蓋了設定檔(第 ⑩ 層),且不會有任何警告
本機 8 秒、k8s CrashLoopBackOff/actuator/startup 的階段耗時;probe 設定CPU limit 太低讓啟動變慢 + initialDelaySeconds 不夠 → 用 startupProbe 分開
啟動很慢BufferingApplicationStartup + /actuator/startup掃描範圍過大、無用 starter 太多、連線池預熱、@PostConstruct 做外部呼叫

練習題 點選選項查看解析

0 / 10
01 / 10
@SpringBootApplication 由哪三個註解組成?魔法主要來自哪一個?
A @Configuration + @EnableWebMvc + @ComponentScan,魔法來自 @EnableWebMvc
B @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan,魔法來自 @EnableAutoConfiguration
C @Component + @Bean + @Import,魔法來自 @Import
D @Configuration + @EnableScheduling + @ComponentScan
解析
@EnableAutoConfiguration 透過 @Import(AutoConfigurationImportSelector.class) 讀取所有 jar 裡的自動組態清單檔(Boot 3 是 AutoConfiguration.imports,2.7 以前是 spring.factories),載入一兩百個候選類別再靠 @Conditional 過濾。
02 / 10
自動組態的運作方式是什麼?
A 只載入你用得到的設定類別
B 先把所有候選自動組態類別全部載入成一份清單,再逐一套用 @Conditional 判斷要不要生效
C 在編譯期就決定好要載入哪些
D 在第一次使用某個 Bean 時才動態建立
解析
本質是「一份很長的候選清單 + 一組過濾條件」。這也是 Spring Boot 啟動比純 Spring 慢的主因之一,所以官方用 spring-autoconfigure-metadata.properties 預先建索引,讓明顯不成立的候選能快速排除。
03 / 10
為什麼自己定義一個 DataSource,Spring 的預設自動組態就不會生效?
A 因為 Spring 會偵測到衝突並拋出例外
B 因為 @ComponentScan 先處理使用者的類別、自動組態最後才處理,此時 @ConditionalOnMissingBean 發現已經有 BeanDefinition 就放棄建立
C 因為使用者定義的 Bean 優先序較高,會在執行期覆蓋
D 因為自動組態只在沒有任何 @Configuration 時才啟用
解析
「讓位」能成立是因為時序:自動組態被刻意安排在最後處理。判斷依據是 BeanDefinition 而不是 Bean 實例,因為這個階段一個 Bean 都還沒被實例化。
04 / 10
BeanFactoryPostProcessor 和 BeanPostProcessor 的差別是什麼?
A 前者處理單例、後者處理 prototype
B 前者改「設計圖」BeanDefinition(此時還沒有物件,可決定不要建立),後者改「成品」Bean 實例(AOP 代理就在這裡)
C 前者是 Spring Boot 專屬、後者是 Spring 原生
D 兩者相同,只是命名不同
解析
這是整個 Spring 擴充機制最重要的一組對比。要影響「要不要建立、建立成什麼樣」用 BFPP(自動組態本身就是靠 ConfigurationClassPostProcessor 這種 BFPP 實作的);要影響「建立出來之後做什麼」用 BPP(ch07 的 AOP 代理)。
05 / 10
為什麼不該在 @PostConstruct 裡打外部 API 或預熱快取?
A 因為 @PostConstruct 不支援網路操作
B 因為那個階段容器還沒準備好、內嵌容器還沒啟動,外部服務慢或掛掉會讓整個應用卡在啟動階段,而不是「啟動成功但功能降級」
C 因為 @PostConstruct 有 5 秒逾時限制
D 因為會造成循環依賴
解析
把「啟動失敗」變成「啟動成功但功能降級」永遠是比較好的選擇。這類工作屬於 ApplicationRunner 或 ApplicationReadyEvent——那時所有 Bean 已就緒、內嵌容器也啟動了。
06 / 10
k8s 的 Deployment 設了 SPRING_DATASOURCE_URL,而你改了 application.yml 的 spring.datasource.url,結果會如何?
A application.yml 優先,你的修改生效
B 環境變數優先(第 ⑥ 層 > 第 ⑩ 層),你的修改無聲失效,而且不會有任何警告
C 兩者會合併
D 啟動時會報錯提示衝突
解析
這是「本機正常、容器裡不對」最常見的原因。可用 /actuator/env/spring.datasource.url 看它實際從哪一層來、被誰覆蓋。順帶注意 Relaxed Binding 的規則:連字號也要變成底線(my-app.max-retry → MY_APP_MAX_RETRY)。
07 / 10
自己寫的自動組態類別完全沒有作用,--debug 報告裡連 Negative matches 都找不到它。最可能的原因?
A 條件沒有滿足
B 沒有註冊——忘了寫 .imports 檔(或 Boot 3 還在用舊的 spring.factories);自動組態類別不會被 @ComponentScan 掃到
C 缺少 @Configuration 註解
D 版本不相容
解析
「連在 Negative matches 裡都找不到」是關鍵線索——代表它根本沒被載入,問題出在註冊而不是條件。自動組態類別通常不在使用者的套件底下,不會被掃描到,一定要靠 META-INF 的清單檔註冊。
08 / 10
本機啟動 8 秒完全正常,上了 k8s 卻不斷 CrashLoopBackOff 且 log 沒有錯誤,最可能的原因與修法?
A 程式有 bug,需要看更詳細的 log
B CPU limit 太低讓啟動變慢,超過 livenessProbe 的 initialDelaySeconds 而被重啟;用 startupProbe 把「啟動」與「存活」分開,並給足啟動期 CPU
C 映像檔壞掉,需要重新建置
D 資料庫連不上
解析
log 沒有錯誤是關鍵線索——應用根本沒有錯,只是不夠快。這跟 ch04 的 Full GC 停頓導致 liveness 失敗是同一類問題:健康檢查沒有區分「還沒好」與「壞掉了」。startupProbe(k8s 1.18+)就是為此而生。
09 / 10
寫 starter 時為什麼一定要加 @ConditionalOnMissingBean?
A 為了提升啟動效能
B 為了讓使用者能用自己的實作覆蓋你的預設——這是 starter 的基本禮貌
C 因為 Spring 強制要求
D 為了避免循環依賴
解析
沒有它的話,使用者無法客製化,你的 starter 就變成一個無法調整的黑盒子。另外兩個好用的細節:用 @ConditionalOnProperty 配 matchIfMissing = true 提供 enabled 開關;加上 spring-boot-configuration-processor 讓使用者在 IDE 有自動完成。
10 / 10
在自己寫的自動組態裡,為什麼要盡量避免 @ConditionalOnBean?
A 因為它效能較差
B 因為它判斷的是「此刻容器裡有沒有」且之後不會重新評估,對載入順序非常敏感——順序一變條件就不成立且不會報錯
C 因為它在 Spring Boot 3 已被移除
D 因為它只能用在 @Configuration 類別上
解析
徵狀是「加了一個新的自動組態之後,另一個原本正常的 Bean 突然消失」,或本機正常換環境就失效。若必須用,要搭配 @AutoConfigureAfter 明確宣告順序;更穩的做法是改用 @ConditionalOnClass 或 @ConditionalOnProperty。

點擊卡片翻面查看答案,共 17 張。

QUESTION
SpringApplication.run() 的核心是哪一步?
點擊翻面
ANSWER
第 ⑥ 步的 refresh()——而它完全是 Spring 原本就有的東西。Spring Boot 做的是「在 refresh 之前幫你把 BeanDefinition 準備好」(自動組態),加上內嵌容器、starter 依賴聚合與統一的設定機制。
點擊翻回
QUESTION
啟動流程的兩個關鍵分界?
點擊翻面
ANSWER
① BeanDefinition 全部產生完,才開始實例化——所以自動組態能根據「使用者有沒有定義」來讓位。② 內嵌 Tomcat 在所有單例 Bean 建立完之後才啟動——避免沒準備好就接請求,也因此任何 Bean 初始化慢都會變成啟動慢。
點擊翻回
QUESTION
@SpringBootApplication 拆開來是什麼?
點擊翻面
ANSWER
@SpringBootConfiguration(就是 @Configuration)+ @ComponentScan(掃主類別所在套件)+ @EnableAutoConfiguration(魔法來源)。
點擊翻回
QUESTION
★ 自動組態的本質是什麼?
點擊翻面
ANSWER
一份很長的候選清單 + 一組過濾條件。先把所有 jar 裡宣告的自動組態類別全部載入(通常一兩百個),再逐一套用 @Conditional 判斷要不要生效。
點擊翻回
QUESTION
自動組態清單檔放在哪?Boot 3 和 2.7 以前有何不同?
點擊翻面
ANSWER
Boot 3.x:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。2.7 以前:META-INF/spring.factories。Boot 3 已不再讀舊的 EnableAutoConfiguration key。
點擊翻回
QUESTION
為什麼自己定義的 Bean 會贏過自動組態?
點擊翻面
ANSWER
時序。ConfigurationClassPostProcessor 先處理 @ComponentScan(使用者的類別),最後才處理 @Import 進來的自動組態,此時 @ConditionalOnMissingBean 發現已有 BeanDefinition 就放棄。判斷的是 BeanDefinition 不是實例。
點擊翻回
QUESTION
★ BeanFactoryPostProcessor vs BeanPostProcessor?
點擊翻面
ANSWER
BFPP 改「設計圖」BeanDefinition(還沒有物件,可改類別/作用域/建構參數,甚至決定不建立);BPP 改「成品」Bean 實例(AOP 代理就在這裡)。要影響「建不建、建成什麼樣」用 BFPP。
點擊翻回
QUESTION
為什麼不該在 @PostConstruct 做外部呼叫?
點擊翻面
ANSWER
那時容器還沒準備好、內嵌容器還沒啟動,外部服務慢或掛掉會讓整個應用卡在啟動階段。「啟動成功但功能降級」永遠好過「啟動失敗」。改用 ApplicationRunner 或 ApplicationReadyEvent。
點擊翻回
QUESTION
設定優先順序的前三名與最常出事的一層?
點擊翻面
ANSWER
命令列參數 > SPRING_APPLICATION_JSON > ServletConfig…;最常出事的是第 ⑥ 層「作業系統環境變數」,它高於所有 application.yml(第 ⑨⑩ 層),在 k8s 裡會無聲覆蓋設定檔且沒有任何警告。
點擊翻回
QUESTION
Relaxed Binding 最容易寫錯的地方?
點擊翻面
ANSWER
連字號也要變成底線。my-app.max-retry-count 對應的是 MY_APP_MAX_RETRY_COUNT,不是 MYAPP_MAXRETRYCOUNT。
點擊翻回
QUESTION
怎麼確認某個設定值實際從哪來?
點擊翻面
ANSWER
/actuator/env/{屬性名} 會顯示它出現在哪些 PropertySource、哪一個生效。/actuator/configprops 看所有 @ConfigurationProperties 的實際值。正式環境記得保護這些端點。
點擊翻回
QUESTION
「某個 Bean 沒被建立」第一個該看什麼?
點擊翻面
ANSWER
--debug 的 CONDITIONS EVALUATION REPORT。Negative matches 會直接告訴你哪個條件沒過。比在網路上搜尋錯誤訊息有效得多。
點擊翻回
QUESTION
自訂自動組態毫無作用,且報告裡連 Negative matches 都沒有它,代表什麼?
點擊翻面
ANSWER
它根本沒被載入——問題出在註冊不在條件。自動組態類別不會被 @ComponentScan 掃到(通常不在使用者套件下),必須靠 META-INF 的 .imports 或 spring.factories 註冊。
點擊翻回
QUESTION
本機 8 秒、k8s CrashLoopBackOff 且 log 無錯誤,怎麼修?
點擊翻面
ANSWER
CPU limit 太低讓啟動變慢,超過 livenessProbe 的 initialDelaySeconds 被重啟。用 startupProbe 把「啟動」與「存活」分開(failureThreshold × periodSeconds 給足),並給啟動期足夠 CPU。
點擊翻回
QUESTION
寫 starter 的三個必備細節?
點擊翻面
ANSWER
① 永遠加 @ConditionalOnMissingBean(讓使用者能覆蓋,這是基本禮貌)② 用 @ConditionalOnProperty 配 matchIfMissing = true 提供 enabled 開關 ③ 加 spring-boot-configuration-processor 讓 IDE 有自動完成。命名用 xxx-spring-boot-starter。
點擊翻回
QUESTION
為什麼自己寫的自動組態要避免 @ConditionalOnBean?
點擊翻面
ANSWER
它判斷「此刻有沒有」且之後不重新評估,對載入順序極度敏感——順序一變條件就不成立,而且不報錯。必要時搭配 @AutoConfigureAfter;更穩的是改用 @ConditionalOnClass 或 @ConditionalOnProperty。
點擊翻回
QUESTION
★ 這一軌八章的共同主線?
點擊翻面
ANSWER
① 每一層都有看不見的成本(磁碟 I/O、建立連線、STW、主記憶體存取),找出成本設計理由就會浮現 ② 最危險的 bug 是不會報錯的那種,所以要記徵狀 ③ 先找根因再調旋鈕,倒過來做下週會再出一次事。
點擊翻回