跳到正文
Joeplover
学习笔记·2026-07-09·约 4 分钟阅读

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 这些概念就不再是一团迷雾了。