這是這一軌的最後一章,也是把前面七章串起來的地方。
Spring Boot 最大的賣點是「約定優於配置」——引入一個 spring-boot-starter-data-jpa,什麼都不用設定,DataSource、EntityManagerFactory、交易管理器就全部有了。
這很方便,但代價是當它不如預期時,你完全不知道要從哪裡找起:
① 引入了 starter,但那個 Bean 就是沒有被建立
② 自己定義了一個 DataSource,結果 Spring 的預設設定全部消失了
③ 本機讀到的設定值是 A,容器裡跑出來卻是 B
④ 本機啟動 8 秒,上了 Kubernetes 一直 CrashLoopBackOff
這四種狀況沒有一個會給你清楚的錯誤訊息。要能診斷它們,就得知道啟動的那幾秒鐘裡到底發生了什麼。
這一章的路徑:
main() 到能接請求的完整階段,以及每個階段可以插手的擴充點。@Conditional 過濾——理解這個「先全載入再過濾」的順序,前面第 ①② 種狀況就能自己推理。@ConditionalOnMissingBean 的讓位機制:為什麼你自己寫一個 Bean 就能覆蓋官方的預設。這章會用到 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 —— 可以接請求了@SpringBootApplication
= @SpringBootConfiguration (就是 @Configuration)
+ @ComponentScan (掃描主類別所在套件及其子套件)
+ @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 才建立把因果鏈接起來:
spring-boot-starter-data-jpa@ConditionalOnClass 的條件成立一兩百個候選類別都要被載入並評估條件,這是 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 了」→ 放棄建立@ConditionalOnClass / @ConditionalOnMissingClass——classpath 上有沒有某個類別@ConditionalOnBean / @ConditionalOnMissingBean——容器裡有沒有某個 Bean@ConditionalOnProperty——設定檔裡某個屬性的值(最常用來做開關)@ConditionalOnWebApplication / @ConditionalOnNotWebApplication@ConditionalOnResource——某個檔案存不存在@ConditionalOnExpression——SpEL 表達式(最靈活,但也最難讀)@ConditionalOnBean 對「順序」非常敏感。@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'啟動流程的每個階段都留了插手的地方。選錯階段是很常見的問題——因為在錯的階段做對的事,結果就是沒有效果。
執行時機:Environment 準備好之後,Context 建立之前
用途:動態加入 PropertySource,例如從設定中心拉設定
註冊:必須寫在 spring.factories/.imports 裡(此時容器還不存在,不能用 @Component)執行時機:Context 建立之後、refresh() 之前
用途:對 ApplicationContext 做設定(例如註冊額外的 PropertySource、設定 profile)
註冊:同樣要寫在 spring.factories/.imports執行時機:所有 BeanDefinition 註冊完,Bean 實例化之前
用途:修改 BeanDefinition,甚至動態註冊新的 Bean
★ 自動組態本身就是靠 ConfigurationClassPostProcessor(一種 BFPP)實作的執行時機:每個 Bean 初始化前後(ch06 生命週期的第 ⑤⑦ 步)
用途:包裝或替換 Bean 實例
★ AOP 代理就是靠這個做的(ch07)執行時機:所有 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()SPRING_DATASOURCE_URL,而你改了 application.yml 裡的 spring.datasource.url 卻完全沒有作用——因為環境變數(第 ⑥ 層)優先於設定檔(第 ⑩ 層)。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 的實際值# ❌ 以為會「合併」
application.yml: spring.datasource.hikari.maximum-pool-size: 10
application-prod.yml: spring.datasource.hikari.connection-timeout: 3000
# 對於 map/list 型別,高優先的 PropertySource 會「整個取代」而不是逐項合併
# 純量屬性則是逐項覆蓋(上面這個例子其實兩個都會生效)# 方法一:啟動時的階段耗時
java -jar app.jar --debug
# 方法二:Spring Boot 2.4+ 的啟動追蹤(最精確)
@Bean
ApplicationStartup applicationStartup() {
return new BufferingApplicationStartup(2048);
}
# 然後看 /actuator/startup —— 會列出每個步驟的耗時
# 方法三:看哪些 Bean 建立得慢
--debug 的 log 裡搜尋 "Bean creation" 的時間戳@ComponentScan 範圍過大——直接掃 com 之類的根套件,掃描與條件評估的時間暴增。維持 Spring Boot 的預設(主類別所在套件)。--debug 看 Positive matches 裡有沒有你根本沒在用的。minimum-idle 設得高時,啟動要先建立這些連線;如果資料庫在遠端,每條連線幾十毫秒就會累積起來。@PostConstruct 裡做外部呼叫——這是最糟的一種,見下面。CrashLoopBackOff,log 看起來很正常、只是還沒印完就被砍掉。livenessProbe 的 initialDelaySeconds 設 10 秒# ① 用 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=truestartupProbe 就是為了解決這個而生的。寫一次 starter 是驗證前面理解的最好方式——因為它會用到自動組態、條件註解、設定綁定與載入順序。
spring-boot-starter-xxxxxx-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.factories——3.x 已經不再讀取 EnableAutoConfiguration 那個 key--debug 看 CONDITIONS EVALUATION REPORT,如果你的類別連在 Negative matches 裡都找不到,代表它根本沒被載入——問題出在註冊,不在條件。@ConditionalOnMissingBean——讓使用者能覆蓋你的預設,這是 starter 的基本禮貌enabled 開關(@ConditionalOnProperty 配 matchIfMissing = true)——預設啟用但隨時能關掉spring-boot-configuration-processor——編譯時產生中繼資料,讓使用者在 IDE 裡有自動完成與說明Spring Boot 讓你不用寫設定,代價是「不知道自己在用什麼」。這在順利時完全不是問題,出事時卻是最大的障礙。
--debug——條件評估報告是這套魔法的說明書@PostConstruct 做外部呼叫——把「啟動失敗」變成「啟動成功但功能降級」,永遠是比較好的選擇@SpringBootApplication(exclude = XxxAutoConfiguration.class)| 階段 | 發生什麼 | 擴充點 | 適合做什麼 |
|---|---|---|---|
| ② 準備 Environment | 組成 PropertySource 清單 | EnvironmentPostProcessor | 從設定中心拉設定;此時設定值還沒被任何人讀走 |
| ⑤ 準備 Context | Context 建好、尚未 refresh | ApplicationContextInitializer | 註冊額外 PropertySource、設定 profile |
| ⑥ refresh 前半 | 產生所有 BeanDefinition(還沒有實例) | BeanFactoryPostProcessor | 改「設計圖」:改類別/作用域/建構參數,或決定不要建立 |
| ⑥ refresh 後半 | 實例化所有單例 Bean | BeanPostProcessor | 改「成品」:包裝或替換實例——AOP 代理就在這裡(ch07) |
| ⑦ 啟動完成後 | Bean 就緒、內嵌容器已啟動 | ApplicationRunner / ApplicationReadyEvent | 資料初始化、暖機、一次性任務——不要放在 @PostConstruct |
| 順位 | 來源 | 範例 | 備註 |
|---|---|---|---|
| ① 最高 | 命令列參數 | --server.port=9090 | 臨時覆蓋最方便 |
| ⑤ | System Properties | -Dserver.port=9090 | JVM 層級 |
| ⑥ | 作業系統環境變數 | SERVER_PORT=9090 | 容器/k8s 最常用;會無聲覆蓋設定檔,事故重災區 |
| ⑦⑧ | application-{profile}.yml | application-prod.yml | 打包外優先於打包內 |
| ⑨⑩ | application.yml | — | 你平常改的那個,優先序其實很低 |
| ⑫ 最低 | setDefaultProperties() | — | 程式碼寫死的預設值 |
| 狀況 | 第一個該看的地方 | 常見原因 |
|---|---|---|
| 某個 Bean 沒被建立 | --debug 的 CONDITIONS EVALUATION REPORT 的 Negative matches | jar 不在 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 做外部呼叫 |
點擊卡片翻面查看答案,共 17 張。