Spring Boot 启动原理深度拆解:从 spring.factories 到 @ConditionalOnClass
从 CommandLineRunner 到 @Retention,从 spring.factories 到 AutoConfiguration.imports,从 classpath 到 JVM 内存布局 —— 一次完整的 Spring Boot 启动原理学习记录。
前言
这次学习是从一个简单的 CommandLineRunner 的 run 方法开始的,结果一路追问到了 JVM 内存布局。整个学习过程梳理如下。
一、CommandLineRunner:Spring Boot 启动后的钩子
@Component
public class RedisChecker implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
redisTemplate.getConnectionFactory().getConnection().ping();
}
}
CommandLineRunner 是 Spring Boot 提供的一个接口,只有一个方法 run。它的作用是在 Spring 容器完全初始化完毕(所有 Bean 创建好、依赖注入完成、内嵌 Tomcat 启动之后)自动执行。
如果没有它,你想在启动时做点事(比如检查 Redis 通不通),写在 main 方法里的话 Spring 容器还没建好,拿不到 Bean。
二、注解(Annotation)的定义
注解本质上就是一个"标签"。用 @interface 关键字定义:
public @interface MyTag {}
定义注解时,还可以用元注解来约束它的行为:
@Target — 限制这个注解能贴在哪
@Target(ElementType.TYPE) // 只能贴在类/接口/枚举上
@Target(ElementType.METHOD) // 只能贴在方法上
@Target(ElementType.FIELD) // 只能贴在字段上
@Retention — 这个注解活多久,有三个级别:
- SOURCE:只在源码里有效,编译成 .class 时就被丢弃。例:
@Override,编译器检查完重写就没用了 - CLASS:保留在 .class 文件里,但 JVM 加载到内存时不读进去。给字节码工具(如 CGLIB、ByteBuddy)用的
- RUNTIME:一直保留到运行时,JVM 加载类时放到方法区里,反射能查到。例:
@Autowired
三个级别的本质区别在于能省则省—— SOURCE 最省内存(编译完就丢),RUNTIME 最占(要加载到方法区),按需选择。
注解在 JVM 内存中的位置
运行时,RUNTIME 注解的信息存在方法区(元空间)里。反射调用 getAnnotations() 就是去方法区查。
@MyRuntime("hello") // 方法区里有这条记录
@MyClass("world") // .class 文件里有,但方法区里没有
三、Spring Boot 启动全流程
第一步:判断应用类型
Spring Boot 推断你是 Servlet Web(Tomcat)、Reactive Web(Netty)还是非 Web 应用。通过检查 classpath 上有没有特定类来判断。
第二步:读取 spring.factories / AutoConfiguration.imports
Spring Boot 去所有 jar 的 META-INF/ 目录下找特定文件:
- Spring Boot 2.x:
META-INF/spring.factories - Spring Boot 3.x:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
这个文件里是一份自动配置类的清单,比如:
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
第三步:@EnableAutoConfiguration 触发筛选
@EnableAutoConfiguration 是开关,它导入 AutoConfigurationImportSelector,这个类读取上面的清单,逐行判断。
第四步:@ConditionalOnClass 做条件检查
Spring 对清单里的每一行检查:
@AutoConfiguration
@ConditionalOnClass(RedisTemplate.class) // classpath 上有 RedisTemplate 吗?
@ConditionalOnMissingBean(RedisTemplate.class) // 你没自己定义吧?
public class RedisAutoConfiguration { ... }
关键理解:spring-boot-autoconfigure 这一个 jar 里包含了所有功能的自动配置类(Redis、MongoDB、JDBC、RabbitMQ……几百个)。 它是个大而全的包,无论你用什么 starter,它都会被引入。所以 Spring 不能"有 jar 就加载"—— 同一个 jar 里有几百个配置类,必须逐个判断哪些条件满足。
@ConditionalOnClass(RedisTemplate.class) 检查的不是自己 jar 里的类,而是别的 jar(spring-data-redis.jar)里有没有 RedisTemplate。如果那个 jar 不在 classpath 上,条件就不满足,自动跳过。
关于 classpath
classpath 不是 JVM 内存里的一个区域,而是 JVM 启动时指定的**"找 .class 文件的路径清单"**。
Maven 负责把 pom.xml 中声明的依赖下载到 .m2/repository,编译时把它们的路径拼成 classpath 传给 JVM。Spring 只从 classpath 上读已经存在的东西,它不知道 Maven 是什么,也不知道 .m2 目录在哪。
流程:
pom.xml 声明依赖
→ Maven 下载 jar 到 .m2/repository
→ Maven 把 jar 路径拼成 classpath 传给 java 命令
→ Spring 启动,扫描 classpath 上的所有 jar
→ 读每个 jar 的 META-INF/ 下的特定文件
→ 拿到自动配置清单,逐条用 @ConditionalOnClass 判断
→ 条件满足的加载配置类、创建 Bean
总结
Spring Boot 的自动配置不是魔法,本质就是一张清单 + 条件判断。每个 starter 在自己的 jar 里声明"我有哪些自动配置",Spring Boot 统一读取并按条件筛选。理解了这个,再看 @EnableAutoConfiguration、@ConditionalOnClass、spring.factories 这些概念就不再是一团迷雾了。