【SpringBoot系列-01】Spring Boot 啟動原理深度解析
大家好!今天咱們來好好聊聊Spring Boot的啟動原理。估計不少人跟我一樣,剛開始用Spring Boot的時候覺得這玩意兒真神奇,一個main方法跑起來就啥都有了。但時間長了總會好奇:這背后到底發生了啥?
1. 啟動流程源碼分析
咱們先從最熟悉的入口開始,就是那個帶著@SpringBootApplication
注解的main方法:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {// 這句就是啟動的核心,咱們今天就圍著它轉SpringApplication.run(DemoApplication.class, args);}
}
就這么一行代碼,背后卻藏著大學問。咱們先來看個整體的流程圖,有個宏觀認識:
run()方法里的關鍵步驟
咱們直接看SpringApplication.run()
方法的源碼(基于2.7.x版本):
public ConfigurableApplicationContext run(String... args) {// 計時器,記錄啟動時間,調試時很有用StopWatch stopWatch = new StopWatch();stopWatch.start();// 初始化應用上下文和異常報告器ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();// 配置headless模式,一般用于服務器環境,不需要顯示器等外設configureHeadlessProperty();// 第一步:獲取并啟動監聽器SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();try {// 第二步:準備應用參數ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 第三步:準備環境(重點!)ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);configureIgnoreBeanInfo(environment);// 打印Banner,就是啟動時那個Spring的logoBanner printedBanner = printBanner(environment);// 第四步:創建應用上下文(重點!)context = createApplicationContext();// 第五步:準備異常報告器exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class }, context);// 第六步:預處理上下文(重點!)prepareContext(context, environment, listeners, applicationArguments, printedBanner);// 第七步:刷新上下文(最核心!)refreshContext(context);// 第八步:刷新后的處理afterRefresh(context, applicationArguments);// 停止計時器stopWatch.stop();if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);}// 通知監聽器啟動完成listeners.started(context);// 第九步:執行 runnerscallRunners(context, applicationArguments);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, listeners);throw new IllegalStateException(ex);}try {listeners.running(context);}catch (Throwable ex) {handleRunFailure(context, ex, exceptionReporters, null);throw new IllegalStateException(ex);}// 返回上下文return context;
}
各步驟詳細解析
這段代碼雖然長,但邏輯很清晰。我給你們劃幾個重點:
-
環境準備(prepareEnvironment):這里會加載各種配置,包括application.properties、系統變量、命令行參數等。調試時可以看這里加載了哪些配置源。
-
創建應用上下文(createApplicationContext):根據應用類型(Servlet/Reactive/None)創建不同的上下文。這里有個小技巧,你調試時注意看
ApplicationContext
的具體實現類,Web應用一般是AnnotationConfigServletWebServerApplicationContext
。 -
預處理上下文(prepareContext):這里會加載咱們的主配置類(就是帶
@SpringBootApplication
的那個類)。 -
刷新上下文(refreshContext):這是最核心的一步,里面會完成Bean的掃描、創建、依賴注入等一系列操作。Spring的IoC容器就是在這里真正工作的。
-
執行runners:這是啟動完成前的最后一步,咱們可以在這里做一些初始化工作。
我踩過一個坑,就是在項目啟動慢的時候,不知道哪里出了問題。后來就是在run()
方法里打了斷點,一步步看哪個階段耗時最長,最后發現是某個配置類加載了太多不必要的Bean。所以說,熟悉這個流程對排查問題非常有幫助。
2. SpringApplication初始化過程
咱們剛才看了run()
方法的流程,但在調用run()
之前,SpringApplication
實例的創建也很關鍵。咱們來看它的構造器:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {this.resourceLoader = resourceLoader;// 斷言主源不能為null,否則啟動不了Assert.notNull(primarySources, "PrimarySources must not be null");this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 第一步:判斷應用類型this.webApplicationType = WebApplicationType.deduceFromClasspath();// 第二步:加載初始化器this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 第三步:加載監聽器setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 第四步:推斷主應用類(就是咱們寫main方法的那個類)this.mainApplicationClass = deduceMainApplicationClass();
}
SpringApplication初始化流程圖
為什么要判斷webApplicationType?
這個判斷太重要了!WebApplicationType.deduceFromClasspath()
會根據類路徑上的類來判斷應用類型:
- SERVLET:如果有Servlet相關類且沒有WebFlux相關類,就是普通的Spring MVC應用
- REACTIVE:如果有WebFlux相關類且沒有Servlet相關類,就是響應式應用
- NONE:都沒有,就是普通的非Web應用
這直接決定了后面創建什么樣的ApplicationContext
和嵌入式服務器。比如Web應用會創建TomcatServletWebServerFactory
,而非Web應用就不會。
實際開發中,有時候你明明想創建一個非Web應用,卻因為引入了spring-boot-starter-web依賴,導致它變成了Web應用,啟動時會自動啟動Tomcat。這時候你就可以在啟動類里手動設置:
public static void main(String[] args) {new SpringApplicationBuilder(DemoApplication.class).web(WebApplicationType.NONE) // 強制非Web應用.run(args);
}
初始化器和監聽器是怎么被加載的
注意構造器里的getSpringFactoriesInstances()
方法,這是Spring Boot的一個核心機制。它會去掃描所有jar包下的META-INF/spring.factories
文件,加載里面配置的類。
比如ApplicationContextInitializer
的加載,就是讀取spring.factories
中key為org.springframework.context.ApplicationContextInitializer
的配置。
咱們自己寫starter的時候,也經常用這招。比如想自動注冊一些組件,就可以在自己的starter里放一個spring.factories
文件,配置上需要自動加載的類。
給你們看個小demo,自定義一個初始化器:
// 自定義初始化器
public class MyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("自定義初始化器執行了!");// 可以在這里做一些早期的配置ConfigurableEnvironment environment = applicationContext.getEnvironment();environment.setActiveProfiles("dev"); // 比如強制設置激活的環境}
}
然后在resources下創建META-INF/spring.factories
:
org.springframework.context.ApplicationContextInitializer=\
com.example.demo.MyInitializer
這樣啟動的時候,咱們的初始化器就會被自動加載執行了。是不是很簡單?這招在開發中間件或者通用組件時特別有用。
3. 事件監聽機制與啟動階段劃分
Spring Boot在啟動過程中會觸發一系列事件,這些事件可以幫助我們在不同階段做一些自定義操作。咱們先來看一張表格,了解下主要的事件及其觸發時機:
事件類型 | 觸發時機 | 主要用途 |
---|---|---|
ApplicationStartingEvent | 剛調用run()方法時,在任何處理之前 | 最早的事件,可用于初始化一些非常早期的資源 |
ApplicationEnvironmentPreparedEvent | 環境準備完成,但上下文還沒創建 | 可以修改環境變量,比如添加額外的配置 |
ApplicationContextInitializedEvent | 上下文創建并初始化,但Bean定義還沒加載 | 可以對上下文做一些設置 |
ApplicationPreparedEvent | 上下文準備完成,但還沒刷新 | 可以在Bean加載前做一些操作 |
ApplicationStartedEvent | 上下文刷新完成,Bean已加載,但runner還沒執行 | 可以做一些啟動后的準備工作,如緩存預熱 |
ApplicationReadyEvent | 所有啟動過程完成,應用已可以處理請求 | 通知應用已就緒 |
ApplicationFailedEvent | 啟動失敗時 | 處理啟動失敗的情況,如資源清理 |
事件觸發流程圖
這些事件都是通過SpringApplicationRunListeners
來傳播的。咱們來寫個監聽器的demo,感受一下:
// 監聽啟動完成事件
@Component
public class MyStartupListener implements ApplicationListener<ApplicationStartedEvent> {@Overridepublic void onApplicationEvent(ApplicationStartedEvent event) {System.out.println("應用啟動完成,開始預熱緩存...");// 模擬緩存預熱CacheManager cacheManager = event.getApplicationContext().getBean(CacheManager.class);Cache userCache = cacheManager.getCache("userCache");// 預熱一些常用數據userCache.put(1L, new User(1L, "admin"));System.out.println("緩存預熱完成!");}
}// 緩存配置
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic CacheManager cacheManager() {return new ConcurrentMapCacheManager("userCache");}
}// User類
public class User {private Long id;private String name;public User(Long id, String name) {this.id = id;this.name = name;}// getter和setter方法public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
這個例子中,我們在應用啟動完成后,預熱了用戶緩存,這樣用戶第一次訪問時就不用等數據庫查詢了。這在實際項目中是個很常見的優化手段。
另外,還有個小技巧:如果你的監聽器需要排序執行,可以實現Ordered接口或者加上@Order注解。
4. Bean定義加載過程
Bean的加載可以說是Spring的靈魂了,咱們來看看Spring Boot是怎么加載Bean定義的。
Bean加載流程圖
@ComponentScan的掃描邏輯
@SpringBootApplication
注解里包含了@ComponentScan
,它會掃描指定包下的類,把帶有@Component
、@Service
、@Repository
、@Controller
等注解的類注冊為Bean。
咱們來看下它的核心邏輯(簡化版):
// ComponentScanAnnotationParser的parse方法
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {// 創建掃描器ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);// ... 省略部分代碼 ...// 配置包含過濾器for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {for (TypeFilter typeFilter : typeFiltersFor(filter)) {scanner.addIncludeFilter(typeFilter);}}// 配置排除過濾器(重點注意!)for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {for (TypeFilter typeFilter : typeFiltersFor(filter)) {scanner.addExcludeFilter(typeFilter);}}// 配置掃描的包Set<String> basePackages = new LinkedHashSet<>();String[] basePackagesArray = componentScan.getStringArray("basePackages");for (String pkg : basePackagesArray) {String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);Collections.addAll(basePackages, tokenized);}// 如果沒有指定包,默認使用@ComponentScan所在類的包if (basePackages.isEmpty()) {basePackages.add(ClassUtils.getPackageName(declaringClass));}// 開始掃描并注冊Bean定義return scanner.doScan(StringUtils.toStringArray(basePackages));
}
這里有個地方要特別注意:excludeFilters
會過濾掉某些類。默認情況下,Spring Boot會排除一些特定的類,比如帶有@ConditionalOnMissingBean
等條件注解且條件不滿足的類。
實際開發中,有時候你會發現明明加了@Service
注解的類,卻沒有被注冊為Bean,這時候就要檢查:
- 是不是包掃描路徑不對
- 是不是被某個過濾器排除了
- 是不是有條件注解沒滿足
可以在scanner.doScan()
這里打個斷點,看看掃描結果里有沒有你的類。
自動配置類是怎么被加載的
Spring Boot的自動配置是它最強大的功能之一,這得益于@EnableAutoConfiguration
注解。咱們來看它的源碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";Class<?>[] exclude() default {};String[] excludeName() default {};
}
關鍵就在@Import(AutoConfigurationImportSelector.class)
,這個類會幫我們導入所有符合條件的自動配置類。
自動配置加載流程
AutoConfigurationImportSelector
的核心方法是selectImports()
,它會從META-INF/spring.factories
中加載所有配置的自動配置類(key為org.springframework.boot.autoconfigure.EnableAutoConfiguration
),然后根據條件注解(@Conditional)篩選出符合條件的配置類。
咱們自己寫starter的時候,就是通過這種方式來實現自動配置的。比如mybatis-spring-boot-starter里就有MybatisAutoConfiguration這個自動配置類。
為什么@Configuration注解不能少
為什么必須加這個注解呢?因為Spring在處理@Configuration注解的類時,會通過CGLIB為它創建一個代理對象,這個代理會負責處理@Bean方法之間的依賴關系,確保Bean的單例性。
舉個例子:
// 正確的配置類
@Configuration
public class AppConfig {@Beanpublic ServiceA serviceA() {return new ServiceA();}@Beanpublic ServiceB serviceB() {// 這里會調用serviceA()方法return new ServiceB(serviceA());}
}// 如果不加@Configuration(錯誤示例)
public class AppConfig {@Beanpublic ServiceA serviceA() {return new ServiceA();}@Beanpublic ServiceB serviceB() {// 每次調用serviceA()都會創建新實例!return new ServiceB(serviceA());}
}
如果加了@Configuration,不管調用多少次serviceA(),返回的都是同一個實例(代理會從容器中獲取)。但如果沒加,每次調用都會創建一個新實例,這就違反了Spring的單例原則,可能會導致各種奇怪的問題。
所以記住,配置類一定要加@Configuration注解,別偷懶!
5. 啟動擴展點詳解
Spring Boot提供了很多擴展點,讓我們可以在啟動過程中插入自己的邏輯。咱們來講幾個常用的。
擴展點執行順序圖
CommandLineRunner和ApplicationRunner的區別
這兩個接口都可以用來在應用啟動后執行一些操作,它們的區別主要在參數上:
// CommandLineRunner接收原始的命令行參數
@Component
@Order(2) // 執行順序
public class MyCommandLineRunner implements CommandLineRunner {@Overridepublic void run(String... args) throws Exception {System.out.println("CommandLineRunner執行,參數:" + Arrays.toString(args));// args就是main方法接收的參數數組}
}// ApplicationRunner接收解析后的命令行參數
@Component
@Order(1) // 可以指定執行順序,數字越小越先執行
public class MyApplicationRunner implements ApplicationRunner {@Overridepublic void run(ApplicationArguments args) throws Exception {System.out.println("ApplicationRunner執行");System.out.println("選項參數:" + args.getOptionNames());System.out.println("非選項參數:" + args.getNonOptionArgs());// 獲取特定選項的值if (args.containsOption("debug")) {System.out.println("Debug模式已開啟");}}
}
使用場景建議:
- 如果只是簡單地需要命令行參數,用
CommandLineRunner
更簡單 - 如果需要處理復雜的命令行參數(特別是選項參數),用
ApplicationRunner
更方便 - 可以通過@Order注解指定多個runner的執行順序
BeanPostProcessor的作用
BeanPostProcessor
是Spring中非常強大的一個擴展點,它可以在Bean初始化前后對Bean進行處理。咱們常用的@Autowired、@Value等注解,都是靠它來實現的。
來看個實用的例子:
@Component
public class PerformanceMonitorBeanPostProcessor implements BeanPostProcessor {private Map<String, Long> beanInitTimes = new HashMap<>();// Bean初始化前調用@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 記錄初始化開始時間beanInitTimes.put(beanName, System.currentTimeMillis());return bean;}// Bean初始化后調用@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {Long startTime = beanInitTimes.get(beanName);if (startTime != null) {long initTime = System.currentTimeMillis() - startTime;if (initTime > 100) { // 超過100ms的打印警告System.out.println("警告:Bean [" + beanName + "] 初始化耗時:" + initTime + "ms");}beanInitTimes.remove(beanName);}return bean;}
}
這個例子展示了如何監控Bean的初始化耗時,對于排查啟動慢的問題非常有用。
Spring中的AutowiredAnnotationBeanPostProcessor
就是用來處理@Autowired注解的,它會在Bean初始化前掃描Bean中的@Autowired注解,然后自動注入依賴。
不過要注意,BeanPostProcessor
本身也是Bean,所以定義它的時候不能依賴其他Bean的初始化,否則可能會導致循環依賴問題。
自定義ApplicationContextInitializer
ApplicationContextInitializer
是在Spring上下文初始化之前執行的,它可以用來對上下文進行一些配置。在做中間件適配時特別有用,比如需要統一設置一些上下文屬性。
實現方式很簡單:
public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {// 設置一些系統屬性System.setProperty("spring.profiles.default", "dev");// 添加一些自定義的環境變量ConfigurableEnvironment environment = applicationContext.getEnvironment();Map<String, Object> myProps = new HashMap<>();myProps.put("myapp.version", "1.0.0");myProps.put("myapp.name", "Demo Application");environment.getPropertySources().addLast(new MapPropertySource("myProps", myProps));// 注冊一個BeanFactoryPostProcessorapplicationContext.addBeanFactoryPostProcessor(beanFactory -> {System.out.println("Bean定義數量:" + beanFactory.getBeanDefinitionCount());});System.out.println("自定義ApplicationContextInitializer執行完成");}
}
然后在spring.factories
中注冊:
org.springframework.context.ApplicationContextInitializer=\
com.example.demo.MyApplicationContextInitializer
或者在啟動類中直接注冊:
public static void main(String[] args) {new SpringApplicationBuilder(DemoApplication.class).initializers(new MyApplicationContextInitializer()).run(args);
}
這種方式比監聽器更早執行,適合做一些最早期的配置工作。
6. 常見問題與調試技巧
啟動慢的排查方法
- 開啟DEBUG日志
# application.properties
logging.level.org.springframework=DEBUG
debug=true
- 使用啟動分析工具
// 在main方法中添加
public static void main(String[] args) {System.setProperty("spring.startup.logfile", "startup.log");SpringApplication app = new SpringApplication(DemoApplication.class);app.setApplicationStartup(ApplicationStartup.buffering()); // Spring Boot 2.4+app.run(args);
}
- 常見的啟動慢原因
- 包掃描范圍太大:縮小@ComponentScan的范圍
- 數據源初始化慢:檢查數據庫連接配置
- 不必要的自動配置:使用exclude排除不需要的配置
- Bean初始化慢:優化Bean的初始化邏輯
Bean加載失敗的排查
當遇到Bean找不到或者依賴注入失敗時,可以這樣排查:
- 檢查包掃描路徑
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.example.common"})
- 檢查條件注解
@Component
@ConditionalOnProperty(name = "myapp.feature.enabled", havingValue = "true")
public class MyService {// 如果配置不滿足,這個Bean不會被創建
}
- 查看Bean定義
@Component
public class BeanChecker implements ApplicationContextAware {@Overridepublic void setApplicationContext(ApplicationContext applicationContext) {String[] beanNames = applicationContext.getBeanDefinitionNames();System.out.println("已注冊的Bean數量:" + beanNames.length);for (String beanName : beanNames) {System.out.println(beanName);}}
}
總結
好了,今天咱們把Spring Boot的啟動原理從頭到尾捋了一遍。從main方法開始,到SpringApplication的初始化,再到事件監聽、Bean加載,最后講了幾個常用的擴展點。
其實Spring Boot的啟動過程雖然復雜,但邏輯很清晰,每個階段都有明確的職責。理解了這些原理,不僅能幫我們更好地使用Spring Boot,還能在遇到問題時快速定位原因。
最后給幾個實戰建議:
- 調試啟動問題時,記得在
SpringApplication.run()
方法里打個斷點,一步步看流程 - 想知道哪些自動配置生效了,可以開啟
debug=true
,會打印自動配置報告 - 自定義擴展時,注意選擇合適的擴展點,別在太早的階段做太復雜的操作
- 生產環境中,盡量不要用反射等方式修改Spring的核心流程,容易出問題
- 性能優化時,可以通過BeanPostProcessor監控Bean初始化耗時,找出瓶頸
希望這篇文章能幫到大家,有什么問題歡迎在評論區交流,咱們下次再聊!