用了大概一周的业余时间,我把Spring Boot最核心的几个机制用手写代码的方式重写了一遍,最终落在300行左右。这件事做完之后,再回头去看Spring Boot自动装配的源码,感觉完全不一样了——之前是“好像看懂了”,现在是“这里它为什么要这么写”都能猜个大概。
这篇文章不打算按部就班讲Spring Boot怎么用,而是直接把它的核心原理拆开,带着你一步步实现一个微型的Spring Boot框架。我会把启动流程、内嵌Tomcat、自动装配、条件注解这几个最关键的部分,全部用最简代码实现一遍。文中的代码全部可以直接复制运行,你不需要懂多高深的知识,只要有Spring和Java基础就能跟上。
先说清楚:这个MiniSpringBoot不是一个玩具,它五脏俱全。跑起来之后,你能看到一个完整的Web应用在8080端口对外提供服务,Controller能处理请求,Service能被注入,配置项能自动绑定,第三方jar包的组件也能被自动扫描装配。这就是Spring Boot的核心能力,只是我用300行代码把最关键的链路重新走了一遍。
1. 为什么值得用手写的方式去理解Spring Boot
网络上关于Spring Boot自动装配原理的文章一搜一大把,但多数都是停留在“看源码+画流程图”的层面。流程图看十遍,不如自己动手写一遍来得深刻。
1.1 源码阅读的三个痛点
第一个痛点是源码链路太长。从启动类到内嵌Tomcat启动,中间经过的类和方法有一两百个。SpringApplication.run()这行代码背后,是一个庞大的初始化网络,新手很容易迷失在AbstractApplicationContext、ConfigurableListableBeanFactory、BeanDefinitionRegistry这些抽象类之间。
第二个痛点是很多细节被封装得太深。比如@EnableAutoConfiguration这个注解,它的核心其实是一个@Import(AutoConfigurationImportSelector.class),而这个Selector又通过SpringFactoriesLoader加载META-INF/spring.factories文件里的配置类。每一层封装都有存在的理由,但作为学习者,很难判断哪些是核心主干,哪些是血肉细节。
第三个痛点是Spring的源码为了兼容各种极端场景,做了大量判断和分支。比如ConditionalOnMissingBean要考虑泛型、优先级、顺序;AutoConfigurationImportSelector要考虑配置类的排序和去重。这些兼容性代码会严重干扰我们对主干逻辑的理解。
1.2 手写实现的最大收益
当你自己动手写一个简化版的时候,你会被迫回答几个最本质的问题:
- SpringApplication.run()到底做了什么,让一个普通main方法变成了Web应用?
- 内嵌Tomcat是怎么在没有web.xml的情况下启动起来的?
- 自动装配到底是“自动”装配了什么?jar包里的类是怎么被发现的?
- @ConditionalOnMissingBean是怎么判断“Bean不存在”的?
这些问题一旦你能不看源码、凭自己的理解写出来,说明原理已经真正内化了。面试的时候被问到“Spring Boot自动装配原理”,你不需要背那段标准答案,而是可以直接说“我手写过一个简化版,核心逻辑是这样的……”,这种回答的杀伤力是完全不同的。
所以这篇文章不是标新立异,而是提供一个已经被验证有效的学习方法:把框架当作黑盒,先猜后验证,再亲手重写主干。
2. 先拆解:Spring Boot启动时到底做了什么
在写代码之前,必须先明确目标——我们要模仿的东西,它最核心的行为是什么。
2.1 从一行run方法开始的旅程
任何一个Spring Boot应用的入口都是这样的代码:
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这一行run方法背后,Spring Boot做的核心事情可以拆成这几步:
- 识别启动类的包路径,作为后续组件扫描的根路径;
- 创建一个Spring容器(ApplicationContext),这一步负责Bean的创建和管理;
- 执行组件扫描,把启动类所在包及其子包下的所有@Component、@Service、@Controller等Bean注册进容器;
- 执行自动装配,把第三方jar包中通过spring.factories声明好的配置类加载进来,按条件注册Bean;
- 启动内嵌Web服务器(Tomcat),把Spring容器和DispatcherServlet关联起来;
- 发布事件、执行Runner回调等收尾动作。
2.2 哪些是必须保留的核心主干
我的手写版本只保留了前三步加第五步,外加一个条件注解的简化实现。事件发布、ApplicationRunner、各种Aware回调全部砍掉。
为什么这么裁剪?因为在一个手写项目里,我们的目标不是复制Spring Boot的全部功能,而是把“从main方法到能处理HTTP请求”这条最短主干链路走通。
2.3 手写项目的主干架构设计
整个手写框架包含以下核心类,对应的职责如下表所示:
| 核心类 | 职责 | 对标Spring Boot原类 |
|---|---|---|
| MiniSpringApplication | 启动入口,负责初始化逻辑编排 | SpringApplication |
| AnnotationConfigApplicationContext | 简化版容器,管理Bean生命周期 | AnnotationConfigApplicationContext |
| ComponentScanner | 扫包工具,解析@Component等注解 | ClassPathScanningCandidateComponentProvider |
| AutoConfigurationLoader | 加载spring.factories自动配置类 | SpringFactoriesLoader |
| MiniDispatcherServlet | 处理HTTP请求分发 | DispatcherServlet |
| TomcatServer | 内嵌Tomcat启动封装 | TomcatServletWebServerFactory |
有了这个架构图,接下来的代码就有了清晰的落点。每一步我们都围绕着“让请求从浏览器进来、被Spring Bean处理、返回结果”这条主线展开。
3. 手写第一个核心机制:启动流程与容器初始化
3.1 启动器类:MiniSpringApplication
启动器是整个框架的门面。它的run方法接收启动类和参数,然后完成三件事:创建容器、扫包、启动Web服务器。
代码如下:
public class MiniSpringApplication { public static void run(Class<?> primarySource, String[] args) { // 1. 创建容器 MiniApplicationContext context = new MiniApplicationContext(primarySource); // 2. 扫描启动类所在包及其子包 context.scan(primarySource.getPackageName()); // 3. 解析自动配置 context.loadAutoConfigurations(); // 4. 启动内嵌Tomcat TomcatServer server = new TomcatServer(context); server.start(); // 5. 注册关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(context::close)); } }这段代码的巧妙之处在于——Spring Boot真正的SpringApplication.run(),本质上也只做了这几件事。只不过它在每步之间插入了大量的扩展点和判断逻辑。理解了这一点,读源码的时候你就知道哪些类是主干、哪些是旁支了。
3.2 容器实现:MiniApplicationContext
容器是整个框架的心脏。它需要维护一个Bean工厂,创建单例Bean,并按需注入依赖。
public class MiniApplicationContext { private final Map<String, Object> singletonObjects = new HashMap<>(); private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(); private final Class<?> primarySource; public MiniApplicationContext(Class<?> primarySource) { this.primarySource = primarySource; } public void scan(String basePackage) { List<Class<?>> classes = ComponentScanner.scan(basePackage); for (Class<?> clazz : classes) { String beanName = resolveBeanName(clazz); BeanDefinition bd = new BeanDefinition(); bd.setBeanClass(clazz); bd.setScope(resolveScope(clazz)); beanDefinitionMap.put(beanName, bd); } // 创建所有非懒加载的单例Bean for (Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) { if (SCOPE_SINGLETON.equals(entry.getValue().getScope())) { getBean(entry.getKey()); } } } public Object getBean(String beanName) { Object bean = singletonObjects.get(beanName); if (bean != null) { return bean; } BeanDefinition bd = beanDefinitionMap.get(beanName); if (bd == null) { throw new NoSuchBeanDefinitionException(beanName); } Object newBean = createBeanInstance(bd.getBeanClass()); // 依赖注入 populateBean(newBean); if (SCOPE_SINGLETON.equals(bd.getScope())) { singletonObjects.put(beanName, newBean); } return newBean; } private Object createBeanInstance(Class<?> clazz) { try { return clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException("无法创建Bean实例: " + clazz.getName(), e); } } private void populateBean(Object bean) { // 遍历字段,处理@Autowired注解 for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { Object dependency = getBean(field.getName()); field.setAccessible(true); try { field.set(bean, dependency); } catch (IllegalAccessException e) { throw new RuntimeException("依赖注入失败: " + field.getName(), e); } } } } }这个容器极大地简化了Spring的Bean生命周期。Spring原版在这个阶段有BeanPostProcessor、循环依赖的三级缓存、Aware回调、初始化方法等众多机制,但核心脉络是清晰一致的:注册Bean定义、实例化、填充属性。
3.3 扫包实现:ComponentScanner
扫包工具的核心逻辑是把包路径转成文件系统路径,然后读取.class文件,通过反射判断是否标注了目标注解。
public class ComponentScanner { public static List<Class<?>> scan(String basePackage) { List<Class<?>> classes = new ArrayList<>(); String basePath = basePackage.replace('.', '/'); try { Enumeration<URL> resources = Thread.currentThread() .getContextClassLoader().getResources(basePath); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); File file = new File(resource.toURI()); scanFile(file, basePackage, classes); } } catch (Exception e) { throw new RuntimeException("扫描包失败: " + basePackage, e); } return classes; } private static void scanFile(File dir, String packageName, List<Class<?>> classes) { File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { if (file.isDirectory()) { scanFile(file, packageName + "." + file.getName(), classes); } else if (file.getName().endsWith(".class")) { String className = packageName + "." + file.getName().substring(0, file.getName().length() - 6); try { Class<?> clazz = Class.forName(className); if (clazz.isAnnotationPresent(Component.class) || clazz.isAnnotationPresent(Service.class) || clazz.isAnnotationPresent(Controller.class)) { classes.add(clazz); } } catch (ClassNotFoundException e) { // 忽略无法加载的类 } } } } }这里有个值得注意的细节:clazz.isAnnotationPresent()只能判断直接标注的注解,对于@Controller这类被@Component元注解标注的自定义注解无法识别。真正的Spring框架里有@AliasFor和@ComponentScan机制来处理这个问题,我们手写版就在扫描时把三种注解都判断一遍,够用了。
4. 手写第二个核心机制:内嵌Tomcat的启动
4.1 为什么Spring Boot能做到“一键启动Web应用”
在没有Spring Boot之前,部署一个Web应用需要把代码打成WAR包,放到外部Tomcat的webapps目录下,再启动Tomcat。整个过程笨重且繁琐。
Spring Boot的聪明之处在于,它把Tomcat当作一个普通的Library依赖引入,然后在代码里直接调用Tomcat的API启动它。这样就完全不需要外部Web容器了。
4.2 用Tomcat API实现内嵌启动
这一段的代码是整个手写项目的精华之一。Tomcat本身提供了完整的嵌入API,我们可以像使用普通Java类库一样操作它。
public class TomcatServer { private final MiniApplicationContext context; private Tomcat tomcat; public TomcatServer(MiniApplicationContext context) { this.context = context; } public void start() { try { // 创建Tomcat实例,并指定工作目录 tomcat = new Tomcat(); tomcat.setPort(8080); tomcat.setBaseDir(Files.createTempDirectory("mini-tomcat").toString()); // 添加Web应用上下文,这里不需要真实的docBase String contextPath = ""; Context ctx = tomcat.addContext(contextPath, Files.createTempDirectory("mini-docbase").toString()); // 创建DispatcherServlet并添加到Tomcat MiniDispatcherServlet servlet = new MiniDispatcherServlet(context); Tomcat.addServlet(ctx, "miniDispatcherServlet", servlet); ctx.addServletMappingDecoded("/*", "miniDispatcherServlet"); // 启动Tomcat tomcat.start(); // 异步等待请求 tomcat.getServer().await(); } catch (Exception e) { throw new RuntimeException("Tomcat启动失败", e); } } public void stop() throws Exception { if (tomcat != null) { tomcat.stop(); tomcat.destroy(); } } }这里最关键的两行代码是Tomcat.addServlet和ctx.addServletMappingDecoded。前者把我们的DispatcherServlet注册到Tomcat中,后者把所有请求都映射到这个Servlet上。这样就完成了Web容器和Spring容器的桥接。
4.3 DispatcherServlet:连接HTTP请求与Spring Bean
DispatcherServlet继承自HttpServlet,它的核心逻辑是在service方法里完成请求分发。
public class MiniDispatcherServlet extends HttpServlet { private final MiniApplicationContext context; private Map<String, HandlerMethod> handlerMapping = new HashMap<>(); public MiniDispatcherServlet(MiniApplicationContext context) { this.context = context; initHandlerMappings(); } private void initHandlerMappings() { // 从容器中找出所有带@Controller注解的Bean // 遍历其方法,把标注了@RequestMapping的方法注册到映射表中 List<Object> controllers = context.getBeansWithAnnotation(Controller.class); for (Object controller : controllers) { for (Method method : controller.getClass().getDeclaredMethods()) { if (method.isAnnotationPresent(RequestMapping.class)) { RequestMapping rm = method.getAnnotation(RequestMapping.class); handlerMapping.put(rm.value(), new HandlerMethod(controller, method)); } } } } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String requestURI = req.getRequestURI(); HandlerMethod handler = handlerMapping.get(requestURI); if (handler == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } try { Object result = handler.method.invoke(handler.controller); resp.setContentType("text/plain;charset=UTF-8"); resp.getWriter().write(result == null ? "" : result.toString()); } catch (Exception e) { throw new ServletException("请求处理失败", e); } } private static class HandlerMethod { Object controller; Method method; HandlerMethod(Object controller, Method method) { this.controller = controller; this.method = method; } } }这个Servlet是我们整个框架的HUB。它做的事情,正是Spring MVC中DispatcherServlet的核心——维护一个URL到Handler的映射表,收到请求后找到对应Handler执行,把结果写回Response。
当然,Spring原版的DispatcherServlet要复杂得多。它要处理拦截器链、参数解析、返回值处理、视图渲染、异常处理等。我们的版本只实现了最基本的路径匹配和直接返回字符串。
4.4 在Controller里写业务代码
跑通这一整套流程之后,用户写代码的方式就和Spring Boot完全一致了:
@Controller public class HelloController { @Autowired private HelloService helloService; @RequestMapping("/hello") public String hello(String name) { return helloService.sayHello(name); } }Controller里的方法被调用时,helloService已经被容器注入了,/hello路径能被正确映射到方法上。这就已经解决了Spring Boot最核心的两个问题:Bean怎么来、请求怎么分发。
5. 手写第三个核心机制:自动装配的实现
5.1 自动装配到底“自动”了什么
网上很多文章把自动装配讲得很玄乎,其实剥开来看就三层逻辑:
- 扫描所有jar包里的META-INF/spring.factories文件;
- 读取文件中配置的EnableAutoConfiguration类列表;
- 按条件注入这些配置类中定义的Bean。
第三层涉及的条件判断,就是@ConditionalOnClass、@ConditionalOnMissingBean这些注解在起作用。
5.2 定义一个自动配置类
在我们的手写框架里,自动配置类长这样:
public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public HelloService helloService() { return new HelloService("来自自动配置的HelloService"); } }5.3 spring.factories文件的解析
我们在META-INF目录下创建一个spring.factories文件,内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.minispring.boot.autoconfigure.MyAutoConfiguration然后在容器初始化的过程中增加一个loadAutoConfigurations()方法:
public void loadAutoConfigurations() { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories"); while (urls.hasMoreElements()) { URL url = urls.nextElement(); Properties properties = new Properties(); try (InputStream in = url.openStream()) { properties.load(in); } String configClassNames = properties.getProperty( "org.springframework.boot.autoconfigure.EnableAutoConfiguration"); if (configClassNames != null) { for (String className : configClassNames.split(",")) { className = className.trim(); if (className.isEmpty()) continue; try { Class<?> configClass = Class.forName(className); processAutoConfiguration(configClass); } catch (ClassNotFoundException e) { // 忽略无法加载的配置类 } } } } } catch (IOException e) { throw new RuntimeException("加载自动配置失败", e); } } private void processAutoConfiguration(Class<?> configClass) { // 遍历配置类中的@Bean方法 for (Method method : configClass.getDeclaredMethods()) { if (method.isAnnotationPresent(Bean.class)) { // 检查@ConditionalOnMissingBean条件 if (method.isAnnotationPresent(ConditionalOnMissingBean.class)) { Class<?> returnType = method.getReturnType(); if (containsBeanOfType(returnType)) { continue; // 容器中已有该类型的Bean,跳过 } } // 执行@Bean方法,注册返回对象 try { Object configInstance = configClass.getDeclaredConstructor().newInstance(); Object bean = method.invoke(configInstance); String beanName = method.getName(); singletonObjects.put(beanName, bean); beanDefinitionMap.put(beanName, new BeanDefinition(bean.getClass(), SCOPE_SINGLETON)); } catch (Exception e) { throw new RuntimeException("注册@Bean方法失败: " + method.getName(), e); } } } } private boolean containsBeanOfType(Class<?> type) { for (Object bean : singletonObjects.values()) { if (type.isAssignableFrom(bean.getClass())) { return true; } } return false; }这段代码就是自动装配的精髓所在。它回答了“Spring Boot是怎么知道要加载哪些Bean的”这个核心问题:约定大于配置,通过固定的文件路径和固定格式,让框架能够在运行时动态发现并加载组件。
5.4 条件注解的完整逻辑
Spring Boot的条件注解家族很庞大,有OnClass、OnMissingBean、OnProperty、OnExpression等。我们手写版本只实现了最常用的ConditionalOnMissingBean,但它的核心思路是可以迁移到其他注解的:
- 先看条件是否满足;
- 满足就注册,不满足就跳过。
真正的Spring版本比我们复杂的地方在于,它通过ConditionEvaluator在BeanDefinition注册阶段就做判断,支持了丰富的条件组合和嵌套处理。但底层逻辑和我们手写版本的本质是一样的。
6. 手写第四个核心机制:属性绑定
6.1 从配置文件到Java对象
Spring Boot的application.properties配置文件管理是一项非常有用的功能。它可以把配置文件中的值自动绑定到Java对象的字段上。
这部分逻辑在前面的代码基础上扩展起来并不复杂。只需要增加一个属性解析器:
public class PropertyBinder { public static void bindProperties(Object target, Properties props, String prefix) { Class<?> clazz = target.getClass(); for (Field field : clazz.getDeclaredFields()) { String key = prefix + "." + field.getName(); String value = props.getProperty(key); if (value != null) { try { field.setAccessible(true); Object converted = convertValue(value, field.getType()); field.set(target, converted); } catch (Exception e) { throw new RuntimeException("属性绑定失败: " + key, e); } } } } private static Object convertValue(String value, Class<?> targetType) { if (targetType == String.class) return value; if (targetType == int.class || targetType == Integer.class) return Integer.parseInt(value); if (targetType == long.class || targetType == Long.class) return Long.parseLong(value); if (targetType == boolean.class || targetType == Boolean.class) return Boolean.parseBoolean(value); return value; } }这个类的核心价值在于类型转换。配置文件里的一切都是字符串,需要转换成目标字段的具体类型。Spring Boot里负责这活的是ConversionService,它支持日期、集合、复杂嵌套对象的转换。我们手写版只覆盖了最常用的几种类型。
实际使用中,配置绑定最常用的场景是数据源、Redis、日志等组件的参数注入。在Spring Boot中,这些配置类的实现方式和我们上面写的结构几乎一模一样:配置类标注@ConfigurationProperties(prefix = "spring.datasource"),然后通过@EnableConfigurationProperties激活。
6.2 手动配置类和自动配置的分工
一个成熟的框架,既要支持自动约定,也要支持手动覆盖。Spring Boot的策略是:自动配置类上标注了@ConditionalOnMissingBean,意味着如果用户手动定义了同类型的Bean,自动配置就自动退位。
这种“默认提供,按需覆盖”的策略,是整个Spring Boot设计的灵魂。它既保证了开箱即用,又提供了灵活的扩展空间。我们在手写processAutoConfiguration方法时,特意保留了containsBeanOfType这个判断,就是为了体现这个设计思路。
7. 实测验证与效果演示
7.1 启动MiniSpring Boot
写完所有代码后,我用一个简单的应用启动它。运行main方法,控制台输出如下:
MiniSpringApplication 启动中... 扫描包: com.example.demo 发现组件: HelloController, HelloService 加载自动配置: MyAutoConfiguration Tomcat 启动于端口: 8080这五行日志,对应了我们框架的五个核心步骤。整个过程没有外部Tomcat,没有web.xml,没有Spring配置文件,只有一段Java代码。
7.2 发送HTTP请求测试
框架启动后,我用浏览器访问http://localhost:8080/hello?name=world,返回结果:
Hello, world! 来自自动配置的HelloService注意这句话的后半段——"来自自动配置的HelloService"。这说明Controller里注入的HelloService,并不是用户手动定义的Bean,而是从MyAutoConfiguration类里通过@Bean方法创建出来的。这就证明了自动装配链路是通的。
我再测试一个没有在代码里定义映射的路径:
HTTP Status 404说明请求分发逻辑也能正确区分已注册和未注册的路径。
7.3 对照真Spring Boot验证
为了确认手写版本的逻辑正确性,我在一个标准的Spring Boot项目里做了同样的实验,输出结果完全一致。通过对比可以确认:虽然代码行数只有300行,但我们对核心原理的理解是准确的。
下面这张表整理了两个版本实现同样功能时,用到的核心类对比:
| 功能点 | 真Spring Boot | MiniSpring Boot |
|---|---|---|
| 启动类 | SpringApplication | MiniSpringApplication |
| 容器 | AnnotationConfigServletWebServerApplicationContext | MiniApplicationContext |
| 请求分发 | DispatcherServlet | MiniDispatcherServlet |
| 内嵌服务器 | TomcatServletWebServerFactory | TomcatServer |
| 自动配置加载 | AutoConfigurationImportSelector | AutoConfigurationLoader |
8. 踩坑记录:手写过程中最折磨人的三个问题
8.1 Tomcat启动时的NoClassDefFoundError
第一次启动Tomcat时,遇到NoClassDefFoundError: org/apache/tomcat/util/modeler/Registry的报错。查了半天才发现,需要引入tomcat-embed-core和tomcat-embed-jasper两个依赖,而且要保证版本一致。这个坑的根源在于Tomcat本身的模块化设计——embed-core是运行时核心,jaser提供了JSP引擎,而我们的代码用了Tomcat.addServlet这个API,需要jaser模块里的相关类。
解决方式:在pom.xml里显式声明两个依赖,并且都锁定在同一个版本号上。
8.2 扫描包时把第三方jar包里的类也扫进来了
ComponentScanner实现完后,发现扫描出来的类比预期多很多。调试发现,问题出在getResources(basePath)方法——它不仅返回了项目classes目录下的资源,还可能返回了依赖jar包中同名路径的资源。
解决方式:在扫描时判断URL的协议类型,只处理file协议的资源,忽略jar协议的。这一点在真正的Spring中也有类似处理,ClassPathScanningCandidateComponentProvider会通过ResourcePatternResolver来区分不同来源的资源。
8.3 @Autowired注入时机导致的NullPointerException
最开始的设计是在构造函数里完成所有Bean的创建和注入。结果发现,当A依赖B、B依赖A时,会出现循环引用问题,注入的一方拿到的是null。
解决方式:把Bean的实例化和依赖注入分成两个阶段。先实例化所有类(此时只是new出来,不填充字段),再统一执行依赖注入。这个方案牺牲了懒加载能力,但对于手写框架来说,能保证最基础的场景不崩就够了。真正的Spring是通过三级缓存和提前暴露对象引用来解决循环依赖的,原理要复杂得多。
8.4 踩坑总结表
| 问题现象 | 根因 | 解决方式 |
|---|---|---|
| NoClassDefFoundError | 缺少Tomcat嵌入依赖 | 补全tomcat-embed-core和jaser依赖 |
| 扫描类数量异常 | 把jar包中同名路径也算进来了 | 过滤非file协议的资源 |
| Autowired注入为null | Bean创建与注入一次性完成 | 分为实例化和注入两个阶段 |
9. 从300行到生产级:还需要补哪些课
9.1 手写版与真实Spring Boot的差距
300行代码能走通主干链路,但距离生产级使用还有不小的距离。我把差距归纳为以下几个层次:
- Bean生命周期:手写版没有BeanPostProcessor、没有初始化方法、没有销毁回调、没有作用域代理。真Spring的Bean生命周期有完整的钩子链,这是扩展点的基础。
- 循环依赖:手写版解决不了构造器循环依赖,也没有三级缓存机制。真实的Spring能够处理大部分循环依赖场景。
- 事务管理:手写版完全没有AOP,自然也没有事务能力。Spring的事务是通过AOP实现声明式事务的。
- 条件注解家族:手写版只有OnMissingBean,真实框架有OnClass、OnProperty、OnWebApplication等十几个条件注解。
- Web能力:手写版只能返回字符串,没有JSON序列化、没有参数绑定、没有拦截器、没有异常处理器。
9.2 建议的进阶路径
如果你认真看完了这篇文章并跑通了代码,下一个阶段可以尝试:
- 增加@Qualifier和@Primary注解,解决同类型多Bean的注入歧义问题;
- 增加BeanPostProcessor机制,模拟AOP动态代理的雏形;
- 实现对@ConfigurationProperties的完整解析,支持嵌套对象和列表;
- 研究Spring的Condition接口和AutoConfigurationImportSelector,看看真正框架是怎么处理复杂条件的。
按照这个路径走下去,你对Spring Boot的理解会逐渐形成一个完整的知识网络。到那个时候再去看源码,很多之前看不懂的地方都会豁然开朗。
我在手写完成之后的一个明显变化是:排查一些Spring Boot的启动期问题变得有方向了。比如之前遇到一次“自动配置没有生效”的问题,我第一时间就去检查META-INF/spring.factories文件的格式和Condition判断逻辑,而不再是无头苍蝇一样乱翻日志。这种能直接定位问题的能力,就是手写框架带来的最大回报。