【SpringBoot系列-01】Spring Boot 启动原理深度解析
【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初始化耗时,找出瓶颈
希望这篇文章能帮到大家,有什么问题欢迎在评论区交流,咱们下次再聊!