ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

手写迷你版SpringBoot:透彻理解自动装配与启动流程

2026/9/24 23:10:42 拓冰建站 浏览量
手写迷你版SpringBoot:透彻理解自动装配与启动流程 先说个我自己的判断SpringBoot 学到最后最值钱的部分往往不是背了多少注解而是你能不能把SpringApplication.run()背后发生的事讲清楚能不能解释SpringBootApplication这个组合注解为什么能把那些“约定大于配置”的东西自动拉起来。很多人面试前背了一堆八股结果被问到“自动装配到底是怎么把 RedisTemplate、DataSource 这些 Bean 装进去的”就卡壳了。我这段时间正好把手写的模拟版重新整理了一遍从零搭了一个 mini 版 SpringBoot。本文就把这个过程拆开讲透重点覆盖自动装配和启动流程两条主线。不依赖源码逐行注释而是用“我也能写出来”的思路把核心流程复刻出来。看完你会有一种感觉SpringBoot 表面是框架底层全是 Java 基础功。1. 为什么我要手写一遍SpringBoot核心流程1.1 光背面试题解决不了的问题先说一个常见的场景。你去面试面试官问“SpringBoot 自动装配的原理是什么”标准答案谁都会背EnableAutoConfiguration通过Import导入AutoConfigurationImportSelector然后读取META-INF/spring.factories文件里的配置类再配合ConditionalOnClass之类条件注解按需装配。但你有没有想过几个问题ConditionalOnClass判断“类是否存在”时为什么用Class.forName会抛异常Spring 却能安全判断自动装配得到的 Bean 和业务代码里自己Component声明的 Bean如果类型冲突了谁覆盖谁SpringApplication.run()里先是创建了环境又创建了容器为什么不直接 new 一个AnnotationConfigApplicationContext完事这些问题不深入源码根本答不出所以然。而“手写一遍”恰恰是逼着你去思考这些细节的最快方式。因为当你自己要实现一个类似机制时遇到“类不存在怎么办”“Bean 冲突怎么办”都是躲不开的坎。1.2 手写模拟能学到什么我做的这个 mini 版 SpringBoot 核心功能包括一个MiniSpringBootApplication组合注解一个MiniSpringApplication.run()静态启动方法一套注解驱动的 IOC 容器支持MiniController、MiniService、MiniAutowired内嵌 Tomcat 服务器能注册 Servlet 并处理 HTTP 请求一个简化版AutoConfigurationImportSelector读取mini.factories文件完成自动配置条件注解ConditionalOnClass的基本判断逻辑整套代码大概一千多行每部分都是 Spring Boot 真实机制的精简映射。跑起来后你能在浏览器里访问http://localhost:8080/hello并看到返回的 JSON而这个请求从 HTTP 到 Servlet 再到业务 Controller 的链路每一步都是自己控制的。1.3 模拟和原版差距在哪里先打个预防针模拟版绝不等于源码复刻。真实 SpringBoot 里光一个ConfigurationClassPostProcessor就几千行还有复杂到爆的BeanPostProcessor体系、ConditionEvaluator条件评估流程、ConfigurationClassParser递归解析逻辑。我做的模拟版把这些都砍掉了只保留主干流程。但“砍掉复杂分支保留核心骨架”恰恰是学习复杂框架最好的方式。就像你看一棵大树先看清主干怎么长、养分怎么输送再去研究每片叶子的叶脉才有意义。等你看懂了骨架回头再看源码会发现那些看不懂的类突然有了定位哦原来它是干这个用的。2. 项目整体设计与模拟方案选型2.1 模拟到什么粒度合适动手前最重要的一件事是确定“模拟边界”。如果什么都想模拟比如把 Spring 的BeanDefinitionRegistryPostProcessor机制、FactoryBean、AOP代理全部手动实现一遍那工作量直接爆炸而且偏离了核心目标。我给自己的界定原则是三条只模拟“启动流程”和“自动装配”主链路不碰 AOP、事务等横向能力核心类命名保留原版关键词比如MiniSpringApplication、MiniEnableAutoConfiguration方便对照源码理解能用 JDK 原生反射完成的不引入第三方库这样任何人都能直接跑起来按这个边界我最终采用的启动链路模拟方案是通过MiniSpringApplication.run()发起启动扫描主类所在包及子包收集标注了MiniComponent等注解的类完成依赖注入执行自动配置类装配启动内嵌 Tomcat 服务器触发ApplicationRunner回调每一步都可以对应到真实 SpringBoot 中的一个阶段。后面我们逐个展开说。2.2 需要的环境和依赖我的开发环境是 JDK 8 Maven 3.6 IntelliJ IDEA这套组合兼容性最好JDK 17 也能跑但需要调整个别 API下面会讲到。为了展示方便我不使用 Lombok全部手写 getter/setter 也无所谓因为核心代码量并不大。Maven 依赖只需要一个内嵌 Tomcat。你可能会问为什么模拟 SpringBoot 还要引入 Tomcat因为 SpringBoot 最核心的突破之一就是“内嵌容器”它让 Web 应用不再依赖外部部署的 Tomcat 或 Jetty而是把服务器作为库引入在 Java 代码里直接启动。引入方式很简单dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version8.5.100/version /dependency除此之外不需要 Spring 的任何依赖所有 IOC、AOP 相关容器逻辑全部我们自己写。这才叫“手写模拟”。2.3 项目包结构规划我最终采用的包结构是这样的com.github.minispring ├── MiniSpringApplication.java // 启动器模拟 SpringApplication ├── annotations │ ├── MiniSpringBootApplication.java // 组合注解 │ ├── MiniController.java │ ├── MiniService.java │ ├── MiniComponent.java │ └── MiniAutowired.java ├── context │ ├── MiniApplicationContext.java // IOC 容器核心 │ ├── BeanDefinition.java // Bean 定义 │ └── BeanFactoryPostProcessor.java // 后续扩展预留 ├── autoconfigure │ ├── MiniEnableAutoConfiguration.java │ ├── AutoConfigurationImportSelector.java │ ├── ConditionalOnClass.java │ └── SpringBootCondition.java // 条件判断抽象类 ├── web │ ├── TomcatServer.java // 内嵌 Tomcat 封装 │ ├── DispatcherMiniServlet.java // 请求分发 Servlet │ └── RequestMapping.java // URL 映射注解 └── runner └── MiniApplicationRunner.java // 启动后回调这个结构和真实 SpringBoot 的包布局是对应的读代码时你可以轻松找到“原版对应物”。3. 手动实现IoC容器按下启动流程的启动键3.1 从零设计一个简易BeanFactoryIoC 容器是整个框架的地基SpringBoot 的自动装配再花哨最终干的事情也是把配置类变成 BeanDefinition再实例化成 Bean 放进容器。所以我先写了一个极简的MiniApplicationContext它有下面几个核心字段public class MiniApplicationContext { private final Class? primarySource; // 主配置类 private final MapString, Object singletonObjects new ConcurrentHashMap(); // 单例Bean池 private final MapString, BeanDefinition beanDefinitionMap new ConcurrentHashMap(); private final ListString beanDefinitionNames new ArrayList(); private final Properties environment new Properties(); // 模拟Environment }你可能会问为什么要维护singletonObjects和beanDefinitionMap两份数据因为BeanDefinition存的是“如何创建 Bean”的元信息类名、作用域、是否懒加载而singletonObjects存的是“已经创建好的 Bean 实例”。两个 Map 职责分离才方便后续支持原型模式、懒加载等扩展。3.2 包扫描与BeanDefinition注册原理SpringBoot 启动时默认会扫描启动类所在包及其子包下所有标注了组件注解的类。手动反射扫描的实现要点如下private void scanBeanDefinitions(Class? configClass) throws IOException, ClassNotFoundException { String basePackage configClass.getPackage().getName(); String basePath basePackage.replace(., /); EnumerationURL resources Thread.currentThread().getContextClassLoader() .getResources(basePath); while (resources.hasMoreElements()) { URL url resources.nextElement(); if (!file.equals(url.getProtocol())) continue; File root new File(url.toURI()); for (File file : FileUtils.listFiles(root, new String[]{class}, true)) { String className basePackage . file.getPath() .substring(root.getPath().length() 1) .replace(/, .).replace(.class, ); Class? clazz Class.forName(className); if (clazz.isAnnotationPresent(MiniController.class) || clazz.isAnnotationPresent(MiniService.class) || clazz.isAnnotationPresent(MiniComponent.class)) { // 注册BeanDefinition } } } }这里有一个在真实 Spring 中也会遇到的经典问题如何扫描到子包下的类。我的做法是拿到包路径的 URL如果是file协议就递归遍历目录。这种方法只适用于本地目录部署Spring 里还会处理 jar 包的jar:协议但我们模拟版不需要这么复杂。3.3 依赖注入的几种方式与手写实现Bean 创建出来后接下来要做依赖注入。我实现了两种注入方式字段注入给带有MiniAutowired的字段赋值带参构造器注入如果类只有一个构造器且参数上有MiniAutowired就通过构造器创建构造器注入的代码值得看一下因为它能帮你理解为什么 Spring 官方推荐构造器注入private Object createBeanInstance(BeanDefinition bd) throws Exception { Constructor?[] constructors bd.getBeanClass().getConstructors(); // 只有一个构造器且参数不为空尝试构造器注入 if (constructors.length 1 constructors[0].getParameterCount() 0) { Constructor? constructor constructors[0]; Class?[] paramTypes constructor.getParameterTypes(); Object[] args new Object[paramTypes.length]; for (int i 0; i paramTypes.length; i) { args[i] getBean(paramTypes[i]); // 递归处理依赖 } return constructor.newInstance(args); } return bd.getBeanClass().getDeclaredConstructor().newInstance(); }看到这里你会发现依赖注入本质就是“递归的 getBean”。构造器注入的好处是 Bean 在创建时就必须保证所有依赖就位不会出现字段注入那种“先创建后赋值中间状态不确定”的问题。这也是 Spring 团队现在推荐构造器注入的根本原因。3.4 单例池与二级缓存的演进关系在做依赖注入时我遇到了一个你们也会遇到的问题循环依赖。比如 A 依赖 BB 又依赖 A。如果我用“创建 A - 注入 B - 创建 B - 注入 A”这个顺序就会无限递归。真实 Spring 靠三级缓存解决核心是提前暴露“早期引用”。我的模拟版采用了简化版的二级缓存方案一级缓存singletonObjects存放完整的 Bean二级缓存earlySingletonObjects存放尚未完成属性填充的 Bean 实例实现逻辑是这样的Bean 实例化后立即放入earlySingletonObjects然后才开始注入属性和初始化初始化完成后再移到singletonObjects。当 A 需要注入 B 时如果 B 正在创建中就能从二级缓存拿到 B 的早期引用。当然真实 Spring 的三级缓存还多了一层ObjectFactory主要目的是为了支持 AOP 代理。模拟版不需要处理那么复杂所以二级缓存就够了。但理解这层演进关系对后续看源码非常有帮助。4. 自动装配核心机制手写版EnableAutoConfiguration4.1 自动装配要解决的痛点是什么在 Spring 3.x 时代你要用 JdbcTemplate得自己写Bean方法注册要用 Redis又得自己写连接工厂和模板的 Bean。繁琐程度堪比“点外卖还要自己种菜”。SpringBoot 的自动装配要解决的核心问题就是把常用组件的默认配置全部预置好再根据当前 classpath 中实际引入的依赖决定要不要把这些组件激活。你引入了一个 Redis 依赖自动配置就创建RedisTemplateBean你没引入那就一个多余 Bean 都不产生。我们在模拟版里就把这个思路落到了实处。4.2 条件注解自动装配的“智能开关”自动装配最大的难点不是创建 Bean而是“按需创建”。这就需要条件注解。 我先定义了一个注解和一个判断抽象类Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface ConditionalOnClass { String value(); // 例如 redis.clients.jedis.Jedis }判断逻辑的核心点在 Class 加载方式上。如果直接用Class.forName(xxx)类不存在时会抛出ClassNotFoundException这就破坏了自动装配“安静跳过”的能力。所以真实的 Spring 使用ClassUtils.isPresent()方法它内部捕获了异常并返回 falsepublic abstract class SpringBootCondition { public boolean matches(ConditionContext context, String className) { try { Class.forName(className, false, context.getClassLoader()); return true; } catch (ClassNotFoundException e) { return false; } } }注意这里Class.forName的第二个参数false它表示加载类时不执行静态代码块。这个细节很重要如果类初始化需要依赖其他类而我们只是想“探一下是否存在”初始化时机过早可能导致意外错误。Spring 内部也是使用initializefalse的方式加载。4.3 读取mini.factories完善自动配置清单有了条件判断能力下一步是如何获取“自动配置类的候选清单”。真实 SpringBoot 2.x 里这份清单在META-INF/spring.factories文件里key 是org.springframework.boot.autoconfigure.EnableAutoConfigurationvalue 是一串逗号分隔的类名。我的模拟版仿照这个设计在resources/META-INF/mini.factories中配置了三个测试用的自动配置类org.minispring.boot.autoconfigure.EnableAutoConfiguration\ com.github.minispring.autoconfigure.RedisAutoConfiguration,\ com.github.minispring.autoconfigure.JdbcAutoConfiguration,\ com.github.minispring.autoconfigure.WebMvcAutoConfiguration读取逻辑也很直白用Properties.load()加载文件拿到 key 对应的类名列表然后逐个尝试加载和条件判断public ListString getAutoConfigurationClasses() { ListString result new ArrayList(); Properties props new Properties(); try (InputStream in this.getClass().getClassLoader() .getResourceAsStream(META-INF/mini.factories)) { props.load(in); String value props.getProperty(AutoConfigurationImportSelector.class.getName()); // 按逗号分割去掉空白 result.addAll(Arrays.asList(value.split(,))); } catch (IOException e) { throw new IllegalStateException(mini.factories 加载失败, e); } return result; }4.4 自动配置类的装配流程实战演示现在把整个自动配置的入口打开。我在MiniSpringApplication.run()中主动调用了一个方法模拟EnableAutoConfiguration触发自动装配的流程private void invokeAutoConfiguration() throws Exception { AutoConfigurationImportSelector selector new AutoConfigurationImportSelector(); ListString classNames selector.getAutoConfigurationClasses(); for (String className : classNames) { Class? clazz Class.forName(className, false, this.getClass().getClassLoader()); // 判断类级别的条件注解 ConditionalOnClass condition clazz.getAnnotation(ConditionalOnClass.class); if (condition ! null) { SpringBootCondition evaluator new SpringBootCondition(); if (!evaluator.matches(this, condition.value())) { System.out.println([自动装配] 跳过 className 缺少类: condition.value()); continue; } } // 满足条件后将配置类中的 MiniBean 方法注册为 BeanDefinition processAutoConfigurationClass(clazz); } }processAutoConfigurationClass会遍历配置类里的MiniBean方法把方法返回类型注册成 BeanDefinition创建逻辑是一个“工厂方法调用”Object result method.invoke(beanFactory.createInstance(configClass));到这里你如果引入了 Redis 相关依赖RedisAutoConfiguration就会被激活容器里自动出现一个RedisTemplate的 Bean没引入依赖这个类就会被静默跳过。这正是 SpringBoot “智能”的来源。5. 完整启动流程跑通从注解到内嵌Tomcat5.1 手写启动器总编排与流程时间线现在到了最激动人心的部分把前面所有零散的组件编排在一起跑出一个完整的启动过程。我写的MiniSpringApplication.run()核心代码大约 50 行完整流程分六个步骤public class MiniSpringApplication { public static MiniApplicationContext run(Class? primarySource, String[] args) { System.out.println( Mini Spring Boot 启动中 ); // 1. 打印模拟Banner printBanner(); // 2. 创建上下文对象 MiniApplicationContext context new MiniApplicationContext(primarySource); // 3. 扫描并注册BeanDefinition context.scanAndRegister(); // 4. 执行自动装配 context.invokeAutoConfiguration(); // 5. 创建单例Bean并完成依赖注入 context.finishInit(); // 6. 启动内嵌Tomcat注册DispatcherServlet TomcatServer server new TomcatServer(context); server.start(); // 7. 回调ApplicationRunner context.getBeansOfType(MiniApplicationRunner.class) .forEach(runner - runner.run(args)); System.out.println( Mini Spring Boot 启动完成端口: 8080 ); return context; } }对照真实 SpringBoot 的SpringApplication.run()你会发现主体步骤惊人地相似都是环境准备 - 创建容器 - refresh扫描、注册、实例化- 内嵌服务器 - 发布事件。5.2 refresh方法模拟应用上下文初始化真实 Spring 中AbstractApplicationContext.refresh()是一个模板方法里面十几个步骤各有分工。我的模拟版只保留了四个关键阶段但每个阶段都能和原版对应上public void refresh() throws Exception { // 1. BeanDefinition注册阶段原版: invokeBeanFactoryPostProcessors scanBeanDefinitions(primarySource); // 2. 自动配置阶段原版: EnableAutoConfiguration触发 invokeAutoConfiguration(); // 3. 类型校验和Bean创建前置处理原版: registerBeanPostProcessors prepareBeanPostProcessors(); // 4. 单例Bean实例化与依赖注入原版: finishBeanFactoryInitialization finishInit(); // 5. Web容器启动原版: onRefresh WebServerStartStopLifecycle }这里我特别想说说步骤 5。真实 SpringBoot 中内嵌 Tomcat 的启动并不是在refresh()的最后直接调用tomcat.start()而是通过onRefresh()创建 WebServer 实例再利用 Spring 的生命周期管理在容器刷新完成后自动启动。这个设计把“服务器启动”也纳入了 Spring 容器管理好处是启动顺序、异常处理都能统一收口。我模拟版简化成了直接启动但注释里做了说明。5.3 内嵌Tomcat注册接口实现Tomcat 内嵌启动的代码是这次手写过程中最让我有“原来如此”感受的部分。因为平时我们都用外部 Tomcat从不觉得启动一个 Web 服务器是这么简单的事public class TomcatServer { private final MiniApplicationContext context; public void start() { Tomcat tomcat new Tomcat(); tomcat.setPort(8080); tomcat.getConnector(); // 创建Context容器类似真实SpringBoot中的ServletWebServerApplicationContext StandardContext ctx (StandardContext) tomcat.addWebapp(/, new File(src/main/webapp).getAbsolutePath()); // 注册DispatcherServlet到Tomcat DispatcherMiniServlet servlet new DispatcherMiniServlet(context); Tomcat.addServlet(ctx, dispatcher, servlet); ctx.addServletMappingDecoded(/*, dispatcher); // 启动服务器 tomcat.start(); Runtime.getRuntime().addShutdownHook(new Thread(() - { try { tomcat.stop(); } catch (Exception e) { } })); tomcat.getServer().await(); } }关键点在Tomcat.addServlet(ctx, dispatcher, servlet)和ctx.addServletMappingDecoded(/*, dispatcher)。这两行代码做了真实 Spring Boot 中DispatcherServletRegistrationBean干的事把 SpringMVC 的前端控制器注册到 Servlet 容器并把所有请求都映射给它。5.4 手写DispatcherServlet把请求路由到Controller有了 Tomcat我们还需要一个 Servlet 来处理请求。它做的事情和 SpringMVC 的 DispatcherServlet 几乎一致解析 URL 所映射的 Controller 方法反射调用再把返回值写入响应流。public class DispatcherMiniServlet extends HttpServlet { private final MiniApplicationContext context; Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uri req.getRequestURI(); // 从容器中找到处理该路径的Controller方法 try { Object result route(uri); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write(result ! null ? result.toString() : null); } catch (Exception e) { resp.sendError(500, e.getMessage()); } } private Object route(String uri) throws Exception { for (Object bean : context.getAllBeans()) { Class? clazz bean.getClass(); if (!clazz.isAnnotationPresent(MiniController.class)) continue; String basePath clazz.getAnnotation(MiniController.class).value(); for (Method method : clazz.getMethods()) { RequestMapping rm method.getAnnotation(RequestMapping.class); if (rm ! null) { String fullPath (basePath rm.value()).replace(//, /); if (fullPath.equals(uri)) { // 支持RequestParam参数绑定这里简化为无参调用 return method.invoke(bean); } } } } throw new IllegalStateException(404 Not Found: uri); } }当我第一次通过这个手写 Servlet 在浏览器里访问到自己的 Controller 时那种感觉是单纯的 API 调用给不了的。SpringBoot 的整套“请求 - Servlet - Controller”链路在这一刻才真正在脑海里串成了一条线。5.5 扩展点仿照SpringBoot加入启动回调接口Spring Boot 启动完成后会调用所有ApplicationRunner和CommandLineRunner的实现类用于执行初始化数据、预热缓存等操作。我在模拟版中也增加了对应的扩展点代价只需要定义一个接口public interface MiniApplicationRunner { void run(String... args) throws Exception; }在启动器最后从容器中取到所有该类型 Bean 并依次执行。这个设计体现了 SpringBoot 框架最核心的扩展思想框架负责在恰当的时机调用你暴露的接口你只需要把你的逻辑放进对应实现里。搞清楚这个套路以后学习 SpringCloud 或者其他基于 Spring 的组件时会发现到处都是一样的手法。5.6 测试代码验证自动装配和请求处理的结果我这里写了一个简单测试 Controller 和自动配置类。Controller 长这样MiniController(/hello) public class HelloController { MiniAutowired private HelloService helloService; RequestMapping(/world) public String world() { return {\message\: \ helloService.sayHello() \}; } }自动配置类长这样ConditionalOnClass(com.github.minispring.demo.optional.RedisClient) public class RedisAutoConfiguration { MiniBean public RedisTemplate redisTemplate() { return new RedisTemplate(); } }运行MiniSpringApplication.run(Application.class, args)后控制台输出 Mini Spring Boot 启动中 [扫描] 注册Bean: helloController [扫描] 注册Bean: helloService [自动装配] 激活配置类: RedisAutoConfiguration [自动装配] 注册Bean: redisTemplate [Tomcat] 启动成功端口: 8080 Mini Spring Boot 启动完成 浏览器访问http://localhost:8080/hello/world返回{message: hello from mini spring}。这一刻自动装配不再是一个抽象概念而是你亲眼看到、亲手控制的一系列代码动作。6. 踩坑与排查手写过程中值得记录的三个问题6.1 扫描不到自己写的Controller排查路线是什么第一次运行时遇到最典型的问题是启动类在顶层包com.example.demoController 在子包com.example.demo.controller但容器启动后一个 Bean 都没注册。排查路线不外乎三步看扫描路径拼接对不对包名 相对路径是否拼丢了层级看文件协议如果项目以 jar 包运行file协议取不到目录需要换jar协议处理我这里本地跑所以用 file看注解常量MiniRestController、MiniController名称是否写对注解的Retention是不是RUNTIME我最后发现是substring截取路径时多算了一个前导斜杠类名变成了com.example.demo.controller.HelloControllerClass.forName 自然找不到。这类问题最好的工具就是打印日志每一步扫描到的类名都打出来一眼就能定位。6.2 条件注解导致的自动装配失效第二个典型问题是自动配置类总是被跳过。我在RedisAutoConfiguration上标注ConditionalOnClass(value redis.clients.jedis.Jedis)测试时忘了在 pom 里引入 Jedis 依赖条件判断返回 false配置类就被跳过了。这里有一个值得刻意练习的排查技巧把条件判断结果打印出来比猜“为什么没生效”高效得多。我在自动装配循环里加了日志输出System.out.println([自动装配] 跳过 className 缺少类: condition.value());如果这时候看到跳过日志说明条件判断逻辑正确问题在依赖没引入。如果你没有引入对应类它就不装配这本身就是想要的行为。6.3 类加载器差异导致的资源读取失败第三个坑是在读取mini.factories时我一开始用了MiniSpringApplication.class.getClassLoader().getResourceAsStream()。在普通应用场景下没问题但如果把启动器封装成 jar 又被二次加载就可能导致资源路径权限问题。稳妥的做法是使用Thread.currentThread().getContextClassLoader()ClassLoader cl Thread.currentThread().getContextClassLoader(); try (InputStream in cl.getResourceAsStream(META-INF/mini.factories)) {SpringFramework 内部大量使用ClassUtils.getDefaultClassLoader()来获取类加载器正是为了规避这类跨容器、跨部署场景的类加载器不一致问题。这个经验在后续对接第三方框架时非常受用。6.4 手写模拟与原版启动流程的对应关系速查表我的迷你版实现原版 SpringBoot核心职责MiniSpringApplication.run()SpringApplication.run()启动入口MiniApplicationContextAnnotationConfigServletWebServerApplicationContextIoC 容器invokeAutoConfiguration()EnableAutoConfigurationImportSelector自动配置SpringBootCondition.matches()SpringBootConditionConditionEvaluator条件评估TomcatServer.start()ServletWebServerFactory内嵌服务器DispatcherMiniServletDispatcherServlet请求分发MiniApplicationRunnerApplicationRunner启动后回调这张表对外的一个价值是以后你再看到一个陌生的 SpringBoot 启动相关的类可以试着先把它放进这张表里问问它对应我们手写过程中的哪一步。如果完全对应不上说明它属于横向扩展体系比如 AOP、事务这对定位问题很有帮助。7. 从模拟走向实战你还需要补哪些课手写模拟版获得的最大收获是建立了对 SpringBoot 启动流程和自动装配的整体认知。但如果你想进一步逼近真实框架当前这个版本还有不少明显差距我简单列出几个值得继续深入的方向第一BeanPostProcessor 体系。真实 Spring 中 Bean 的生命周期中穿插了大量后置处理器AOP 的动态代理就是通过AbstractAutoProxyCreator这个BeanPostProcessor在 Bean 初始化后完成的。模拟版完全砍掉了这块后续可以尝试实现一个MiniTransactional用它体验后置处理器的威力。第二EnableAutoConfiguration真正的工作机制。我模拟版是在启动器里手动调用自动装配方法而真实场景是通过Import(AutoConfigurationImportSelector.class)触发这里面涉及到Import如何被ConfigurationClassPostProcessor解析、DeferredImportSelector如何延迟加载等机制。建议去读AutoConfigurationImportSelector源码特别关注它和Conditional的顺序处理。第三Environment 抽象。真实 SpringBoot 支持application.yml、命令行参数、环境变量、配置文件等多种配置源并有一套完整的优先级覆盖机制。模拟版只有一个Properties完全不够用。可以试着实现一个极简的“配置源”接口支持从application.properties和环境变量读取配置再做一次优先级合并这样你对 SpringBoot 配置体系的感悟会更深。第四SpringFactoriesLoader 的缓存机制。实际项目中spring.factories文件数量庞大Spring 在首次读取后会缓存到 Map 里避免每次启动重复 IO。模拟版没有缓存每次启动都读文件你可以试试把读取结果缓存下来甚至仿照SpringFactoriesLoader做一个带Cacheable语义的版本。8. 写在最后的实操心得如果你准备自己动手写一遍我建议直接从最简可运行版本开始不要一开始就追求把所有功能都加上。我第一次写只用了 300 行代码解决了“启动一个空容器”的问题之后再逐步叠加自动装配、Tomcat、DispatcherServlet每加一块都能稳定运行再继续这样排查问题会容易很多。调试顺序也有讲究先跑通 Bean 扫描再加注入再试自动配置再接 Web。每一步通过打印日志确认当前状态比如“注册了哪些 Bean、跳过了哪些配置类”你会惊异地发现SpringBoot 启动时打印的那些日志本质上也是在做同样的事。我对这个 mini 版最满意的地方不是它有多接近原版而是在手写过程中我终于能准确回答“为什么 SpringBoot 启动会比传统 Spring 快”这类问题了。不是因为它启动快而是因为它在启动时把该准备的东西都准备好了你的 bean 一进容器就能直接用而且一切都是按条件精准装配的没有冗余。这套思想理清了以后用它做任何基于 Spring 的二次开发思路都会清晰很多。最后再分享一个写代码时的小技巧类的命名尽量保留原版前缀比如MiniConfigurationClassPostProcessor当我们折腾几天后回头再打开真实源码时你会发现记忆迁移几乎是无痛的因为这些名字就是最好的“代码注释”。建议你也这么来一套自己跑一遍收获会远超预期。