目录
[一、手写模拟 Spring Boot 启动过程](#一、手写模拟 Spring Boot 启动过程)
[1. 核心思想铺垫](#1. 核心思想铺垫)
[2. 手写实现:从注解到容器启动](#2. 手写实现:从注解到容器启动)
[第三步:自定义 Bean 标记注解](#第三步:自定义 Bean 标记注解)
[第四步:手写简化版 Spring 容器](#第四步:手写简化版 Spring 容器)
[3. 原理与踩坑说明](#3. 原理与踩坑说明)
[二、手写模拟 Spring Boot 条件注解功能](#二、手写模拟 Spring Boot 条件注解功能)
[1. 条件注解的核心作用](#1. 条件注解的核心作用)
[2. 核心执行时机](#2. 核心执行时机)
[3. 手写实现 @MyConditionalOnClass](#3. 手写实现 @MyConditionalOnClass)
[4. 集成到包扫描流程](#4. 集成到包扫描流程)
[5. 扩展:@ConditionalOnMissingBean 的实现逻辑](#5. 扩展:@ConditionalOnMissingBean 的实现逻辑)
[三、自动配置功能与 spring.factories 文件解析](#三、自动配置功能与 spring.factories 文件解析)
[1. 自动配置的本质](#1. 自动配置的本质)
[2. spring.factories 文件解析原理](#2. spring.factories 文件解析原理)
[3. 手写自动配置导入逻辑](#3. 手写自动配置导入逻辑)
[第一步:定义 @Import 与 ImportSelector](#第一步:定义 @Import 与 ImportSelector)
[4. 自动配置类示例](#4. 自动配置类示例)
[5. 常见踩坑](#5. 常见踩坑)
[四、Spring Boot 整合 Tomcat 底层源码分析](#四、Spring Boot 整合 Tomcat 底层源码分析)
[1. 内嵌 Tomcat 的核心思想](#1. 内嵌 Tomcat 的核心思想)
[2. 手写内嵌 Tomcat 启动](#2. 手写内嵌 Tomcat 启动)
[第二步:封装 TomcatWebServer](#第二步:封装 TomcatWebServer)
[3. 集成到启动流程](#3. 集成到启动流程)
[4. 源码级细节说明](#4. 源码级细节说明)
[1. 完整执行链路图](#1. 完整执行链路图)
[2. 关键扩展机制](#2. 关键扩展机制)
[3. 用户配置优先的实现](#3. 用户配置优先的实现)
一、手写模拟 Spring Boot 启动过程
1. 核心思想铺垫
Spring Boot 不是一套全新的框架,它本质是对 Spring 容器的 "封装增强",核心解决 Spring 原生开发中配置繁琐、部署依赖外部容器的痛点。它的所有能力,最终都落地到「Bean 的扫描、注册、实例化」这套 Spring 核心流程上。
启动流程的本质可以浓缩为三步:
- 创建 Spring 容器上下文
- 解析启动类,完成包扫描与 Bean 定义注册
- 刷新容器,实例化 Bean,若是 Web 环境则启动内嵌服务器
2. 手写实现:从注解到容器启动
第一步:自定义启动注解
模拟原生 @SpringBootApplication,先实现最基础的包扫描能力(后续再叠加自动配置)。
java
/**
* 自定义启动注解,对标@SpringBootApplication
* 组合包扫描注解,后续会叠加自动配置能力
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponentScan // 引入自定义包扫描注解
public @interface MySpringBootApplication {
}
第二步:包扫描注解定义
对应 Spring 的 @ComponentScan,用于指定扫描路径,默认取启动类所在包。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponentScan {
// 指定扫描包路径,为空则默认扫描启动类所在包
String value() default "";
}
第三步:自定义 Bean 标记注解
对标 @Component,标记哪些类需要被 Spring 容器管理。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
}
第四步:手写简化版 Spring 容器
核心实现「注册配置类 → 包扫描 → Bean 定义注册 → 实例化」的完整流程。
java
/**
* 简化版Spring注解容器,对标AnnotationConfigApplicationContext
*/
public class MyAnnotationConfigApplicationContext {
// Bean定义池:存储所有Bean的Class信息,对应Spring的BeanDefinitionMap
private final Map<String, Class<?>> beanDefinitionMap = new ConcurrentHashMap<>();
// 单例Bean缓存:存储实例化后的单例对象,对应Spring的一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
/**
* 注册配置类(启动类),作为扫描的入口
*/
public void register(Class<?> configClass) {
String beanName = Introspector.decapitalize(configClass.getSimpleName());
beanDefinitionMap.put(beanName, configClass);
}
/**
* 刷新容器:Spring最核心的方法,完成所有Bean的初始化
*/
public void refresh() {
// 1. 执行包扫描,把所有@MyComponent的类注册为Bean定义
scanComponentPackages();
// 2. 实例化所有单例Bean(简化版,暂不处理依赖注入)
instantiateSingletonBeans();
}
/**
* 包扫描核心逻辑
* 读取启动类的包路径,遍历类文件,识别注解,注册Bean定义
*/
private void scanComponentPackages() {
// 获取启动配置类
Class<?> startupClass = beanDefinitionMap.values().iterator().next();
MyComponentScan scanAnnotation = startupClass.getAnnotation(MyComponentScan.class);
// 没指定路径就用启动类所在包,这就是Spring Boot的"约定"
String scanPackage = scanAnnotation.value();
if (scanPackage.isEmpty()) {
scanPackage = startupClass.getPackage().getName();
}
// 把包名转为类路径,读取所有class文件
String classPath = scanPackage.replace(".", "/");
URL resource = startupClass.getClassLoader().getResource(classPath);
if (resource == null) return;
File packageDir = new File(resource.getFile());
// 遍历包下所有class文件
File[] classFiles = packageDir.listFiles(f -> f.getName().endsWith(".class"));
if (classFiles == null) return;
for (File file : classFiles) {
String className = scanPackage + "." + file.getName().replace(".class", "");
try {
Class<?> clazz = Class.forName(className);
// 只有加了@MyComponent的类,才注册进容器
if (clazz.isAnnotationPresent(MyComponent.class)) {
String beanName = Introspector.decapitalize(clazz.getSimpleName());
beanDefinitionMap.put(beanName, clazz);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* 实例化所有单例Bean
* 真实Spring中这一步会处理依赖注入、循环依赖、BeanPostProcessor等扩展
*/
private void instantiateSingletonBeans() {
for (Map.Entry<String, Class<?>> entry : beanDefinitionMap.entrySet()) {
String beanName = entry.getKey();
Class<?> beanClass = entry.getValue();
try {
Object bean = beanClass.getDeclaredConstructor().newInstance();
singletonObjects.put(beanName, bean);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 从容器获取Bean
*/
public Object getBean(String beanName) {
return singletonObjects.get(beanName);
}
public boolean containsBean(String beanName) {
return singletonObjects.containsKey(beanName);
}
}
第五步:手写启动入口类
对标 SpringApplication.run(),封装启动流程
java
public class MySpringApplication {
public static MyAnnotationConfigApplicationContext run(Class<?> primarySource) {
// 1. 创建容器上下文
MyAnnotationConfigApplicationContext context = new MyAnnotationConfigApplicationContext();
// 2. 注册启动类
context.register(primarySource);
// 3. 刷新容器,完成所有Bean初始化
context.refresh();
System.out.println("MySpringBoot 容器启动完成");
return context;
}
}
启动测试
java
// 启动类
@MySpringBootApplication
public class BootDemoApplication {
public static void main(String[] args) {
MyAnnotationConfigApplicationContext context = MySpringApplication.run(BootDemoApplication.class);
// 测试从容器获取Bean
UserService userService = (UserService) context.getBean("userService");
userService.sayHello();
}
}
// 业务Bean
@MyComponent
public class UserService {
public void sayHello() {
System.out.println("hello, my spring boot!");
}
}
3. 原理与踩坑说明
- 为什么要先存 BeanDefinition 再实例化? 因为 Spring 需要先收集全所有 Bean 的元信息,才能处理依赖注入、Bean 的先后顺序、后置处理器扩展。如果边扫描边实例化,会出现依赖的 Bean 还没扫描到的问题。
- 手写版的局限性 :只支持目录形式的类路径,不支持 Jar 包内的扫描;真实 Spring 用
ResourcePatternResolver统一处理各种资源形式。 - 约定大于配置的体现:默认扫描启动类所在包,不用我们手动写包路径,这就是 Spring Boot 最基础的 "约定"。
二、手写模拟 Spring Boot 条件注解功能
1. 条件注解的核心作用
条件注解是自动配置的底层基石:只有满足预设条件时,对应的配置类或 Bean 才会被注册进容器。
- Spring 原生提供
@Conditional元注解 +Condition匹配接口 - Spring Boot 在此基础上扩展了大量常用条件:
@ConditionalOnClass(类存在生效)、@ConditionalOnMissingBean(容器无该 Bean 生效)等
2. 核心执行时机
条件判断发生在 BeanDefinition 注册阶段,不满足条件的类连 Bean 定义都不会进入容器,更不会被实例化,不会占用资源。
3. 手写实现 @MyConditionalOnClass
第一步:定义条件元注解
对标 Spring 的 @Conditional,用于指定条件处理器。
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyConditional {
// 指定条件处理器的Class
Class<? extends MyCondition> value();
}
第二步:定义条件匹配接口
java
public interface MyCondition {
/**
* 条件匹配方法
* @return true表示满足条件,Bean可以注册
*/
boolean matches(Class<?> targetClass);
}
第三步:实现类存在条件处理器
判断类路径下是否存在指定的类,存在则满足条件。
java
public class OnClassCondition implements MyCondition {
@Override
public boolean matches(Class<?> targetClass) {
MyConditionalOnClass annotation = targetClass.getAnnotation(MyConditionalOnClass.class);
if (annotation == null) {
return true; // 没加条件注解,默认放行
}
String targetClassName = annotation.value();
try {
// 反射加载类,加载成功说明类路径下存在
Class.forName(targetClassName);
return true;
} catch (ClassNotFoundException e) {
// 类不存在,不满足条件
return false;
}
}
}
第四步:定义业务条件注解
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MyConditional(OnClassCondition.class) // 绑定条件处理器
public @interface MyConditionalOnClass {
// 用字符串存类名,而不是Class类型
// 原因:如果类不存在,写Class类型编译就会报错,用字符串可以安全反射判断
String value();
}
4. 集成到包扫描流程
在容器的 scanComponentPackages 方法中,注册 Bean 前增加条件校验:
java
// 识别@MyComponent注解
if (clazz.isAnnotationPresent(MyComponent.class)) {
// 执行条件判断,满足条件才注册
if (matchCondition(clazz)) {
String beanName = Introspector.decapitalize(clazz.getSimpleName());
beanDefinitionMap.put(beanName, clazz);
}
}
补充条件判断工具方法:
java
private boolean matchCondition(Class<?> clazz) {
MyConditional conditional = clazz.getAnnotation(MyConditional.class);
if (conditional == null) {
return true;
}
try {
// 实例化条件处理器,执行匹配逻辑
MyCondition condition = conditional.value().getDeclaredConstructor().newInstance();
return condition.matches(clazz);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
5. 扩展:@ConditionalOnMissingBean 的实现逻辑
这个注解是「用户配置优先」的核心:容器中不存在指定 Bean 时,自动配置的 Bean 才会生效。
- 执行时机:必须在所有 Bean 定义都注册完成后、实例化前执行
- 常见坑点 :如果配置类加载顺序不对,会出现判断时用户 Bean 还没注册,导致自动配置错误生效。Spring Boot 通过
@AutoConfigureOrder强制排序来规避这个问题。
三、自动配置功能与 spring.factories 文件解析
1. 自动配置的本质
自动配置不是黑魔法,核心逻辑非常清晰:
- 提前写好一堆配置类(比如 Tomcat 配置、数据源配置)
- 启动时通过配置文件批量发现这些配置类
- 配合条件注解,筛选出适配当前环境的配置类
- 把这些配置类自动注册进容器,创建默认的 Bean
2. spring.factories 文件解析原理
这是 Spring Boot 扩展机制的核心,属于 SPI(服务发现接口)思想的实现。
- 所有 Jar 包都可以在
META-INF/spring.factories中配置自己的自动配置类 - Spring Boot 启动时会扫描所有 Jar 包下的这个文件,聚合所有自动配置类
- 第三方 Starter 就是靠这个文件实现 "引入依赖就自动生效"
手写配置文件
在 resources 目录下创建 META-INF/my-spring.factories:
XML
# 格式:key=自动配置入口的全限定名,value=配置类列表,逗号分隔
com.tuling.myboot.autoconfigure.MyEnableAutoConfiguration=\
com.tuling.myboot.autoconfigure.TomcatAutoConfiguration,\
com.tuling.myboot.autoconfigure.UserAutoConfiguration
手写文件加载工具类
对标 Spring 的 SpringFactoriesLoader。
java
public class MySpringFactoriesLoader {
// 配置文件固定路径
private static final String FACTORIES_PATH = "META-INF/my-spring.factories";
/**
* 加载所有指定类型的配置类全限定名
* @param factoryType 配置key的全限定名
* @param classLoader 类加载器
*/
public static List<String> loadFactoryNames(String factoryType, ClassLoader classLoader) {
List<String> result = new ArrayList<>();
try {
// 1. 扫描类路径下所有的factories文件(包括所有Jar包里的)
Enumeration<URL> urls = classLoader.getResources(FACTORIES_PATH);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 2. 读取properties内容
Properties props = new Properties();
try (InputStream is = url.openStream()) {
props.load(is);
}
// 3. 解析对应key的value,拆分多个类名
String classNamesStr = props.getProperty(factoryType);
if (classNamesStr != null && !classNamesStr.isEmpty()) {
String[] classNames = classNamesStr.split(",");
for (String className : classNames) {
result.add(className.trim());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
3. 手写自动配置导入逻辑
Spring 通过 @Import + ImportSelector 实现批量导入配置类,我们复刻这套逻辑。
第一步:定义 @Import 与 ImportSelector
java
// 导入注解,对标@Import
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyImport {
Class<?> value();
}
// 配置类选择器接口
public interface MyImportSelector {
/**
* 返回需要导入的配置类全限定名数组
*/
String[] selectImports(Class<?> annotationClass);
}
第二步:实现自动配置选择器
java
public class MyAutoConfigurationImportSelector implements MyImportSelector {
@Override
public String[] selectImports(Class<?> annotationClass) {
// 1. 从factories文件加载所有自动配置类
List<String> autoConfigClasses = MySpringFactoriesLoader.loadFactoryNames(
MyEnableAutoConfiguration.class.getName(),
this.getClass().getClassLoader()
);
// 2. 返回给容器,后续会被注册为配置类
return autoConfigClasses.toArray(new String[0]);
}
}
第三步:定义自动配置开启注解
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MyImport(MyAutoConfigurationImportSelector.class) // 导入选择器
public @interface MyEnableAutoConfiguration {
}
第四步:增强启动注解
把自动配置能力组合到启动注解上,和原生 @SpringBootApplication 完全一致。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponentScan
@MyEnableAutoConfiguration // 叠加自动配置能力
public @interface MySpringBootApplication {
}
4. 自动配置类示例
配合条件注解,实现 "满足条件才生效"。
java
/**
* Tomcat自动配置类
* 只有类路径下存在Tomcat相关类时,才会注册进容器
*/
@MyConfiguration
@MyConditionalOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatAutoConfiguration {
@MyBean
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
}
5. 常见踩坑
- 文件格式错误:类名写错、多空格、逗号用了中文格式,都会导致类加载失败,自动配置不生效。
- 条件顺序错误 :
@ConditionalOnClass是类加载级别的判断,要在 Bean 定义注册前执行;@ConditionalOnMissingBean是容器级别的判断,要在所有 Bean 定义注册后执行,顺序不能乱。 - 包扫描不到:自动配置类不在启动类扫描范围内,所以必须靠 spring.factories 机制发现,不能靠包扫描。
四、Spring Boot 整合 Tomcat 底层源码分析
1. 内嵌 Tomcat 的核心思想
传统 SSM 项目需要外部 Tomcat,打 War 包部署;Spring Boot 则是把 Tomcat 封装成一个普通的 Bean,放在 Spring 容器里管理,容器启动时顺便启动 Tomcat,直接打 Jar 包就能运行。
2. 手写内嵌 Tomcat 启动
第一步:引入依赖
XML
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
第二步:封装 TomcatWebServer
对标 Spring Boot 的 TomcatWebServer,封装启动停止逻辑。
java
public class TomcatWebServer implements MyWebServer {
private Tomcat tomcat;
private final int port = 8080;
@Override
public void start() {
try {
// 1. 创建Tomcat核心实例
tomcat = new Tomcat();
// 2. 设置服务端口
tomcat.setPort(port);
// 3. 设置临时工作目录
tomcat.setBaseDir(System.getProperty("java.io.tmpdir"));
// 4. 创建Web应用上下文,对应一个项目
Context context = tomcat.addContext("/", System.getProperty("java.io.tmpdir"));
// 5. 注册Servlet(真实Spring MVC会注册DispatcherServlet)
Tomcat.addServlet(context, "demoServlet", new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("<h1>MySpringBoot 内嵌Tomcat运行成功</h1>");
}
}).setLoadOnStartup(1);
// 6. 配置Servlet映射路径
context.addServletMappingDecoded("/", "demoServlet");
// 7. 启动Tomcat
tomcat.start();
System.out.println("内嵌Tomcat启动完成,端口:" + port);
// 8. 阻塞等待请求,防止主线程退出
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
@Override
public void stop() {
try {
tomcat.stop();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
3. 集成到启动流程
在 MySpringApplication.run 中,容器刷新后启动 Web 服务器:
java
public static MyAnnotationConfigApplicationContext run(Class<?> primarySource) {
MyAnnotationConfigApplicationContext context = new MyAnnotationConfigApplicationContext();
context.register(primarySource);
context.refresh();
// 如果容器中有WebServer Bean,就启动它
if (context.containsBean("tomcatWebServer")) {
MyWebServer webServer = (MyWebServer) context.getBean("tomcatWebServer");
// 异步启动,避免阻塞主线程
new Thread(webServer::start).start();
}
System.out.println("MySpringBoot 启动完成");
return context;
}
4. 源码级细节说明
- WebServer 抽象 :Spring Boot 定义了
WebServer接口,有 Tomcat、Jetty、Undertow 三个实现,通过条件注解自动切换,这就是能无缝更换 Web 容器的原因。 - 启动时机 :真实源码中,Tomcat 启动在
ServletWebServerApplicationContext的onRefresh()方法里,属于容器刷新流程的一环,不是刷新完才启动。 - 零配置原理 :Spring MVC 的
DispatcherServlet也是靠自动配置类自动创建并注册到 Tomcat 的,不用我们写 web.xml。
五、自动配置类加载全流程源码梳理
1. 完整执行链路图

2. 关键扩展机制
- 排除机制 :
@SpringBootApplication(exclude = ...)可以手动排除不需要的自动配置类,本质是在过滤阶段移除。 - 排序机制 :
@AutoConfigureBefore、@AutoConfigureAfter控制配置类加载顺序,比如数据源配置必须在连接池配置之后。 - 属性绑定 :
@ConfigurationProperties把 yml 配置绑定到 JavaBean,自动配置类通过它读取用户自定义配置,覆盖默认值。
3. 用户配置优先的实现
核心就是 @ConditionalOnMissingBean:
- 所有自动配置的默认 Bean 都加了这个注解
- 用户如果自己注册了同类型的 Bean,自动配置的 Bean 就不会创建
- 保证用户自定义的优先级最高,自动配置只做兜底
六、面试速记总结
- Spring Boot 启动核心三步:创建容器 → 包扫描注册 Bean 定义 → 刷新容器实例化 Bean,Web 环境额外启动内嵌 Tomcat。
- 条件注解核心 :
@Conditional是元注解,Spring Boot 扩展了类存在、Bean 缺失等常用条件;判断发生在 BeanDefinition 注册阶段。 - 自动配置本质 :
spring.factoriesSPI 机制批量发现配置类 + 条件注解按需加载,实现第三方 Starter 开箱即用。 - 内嵌 Tomcat 原理:把 Tomcat 封装成容器中的 Bean,随 Spring 容器刷新而启动,替代外部 Web 容器。
- 手写的意义:剥掉 Spring Boot 的层层封装,核心还是注解解析、反射、类加载、SPI 这些 Java 基础技术;理解了底层,排查自动配置失效、启动失败等问题会有所帮助。