ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

手写Spring IoC容器:注解+反射实现依赖注入与循环依赖解决

手写Spring IoC容器:注解+反射实现依赖注入与循环依赖解决 手写Spring IoC注解反射的轻量级容器我看完源码后自己造了一个这两年面试几乎绕不开Spring面试官上来就是“Spring IoC的初始化流程讲一下”“三级缓存解决循环依赖的原理是什么”。源码我也看过不少遍但老实说光看不写很多细节转头就忘。后来我干脆做了一件在不少人看来有点“折腾”的事——不依赖Spring纯用注解和反射手写了一个简化版IoC容器。这一写不要紧Spring里那些经常被背得滚瓜烂熟的概念比如BeanDefinition、单例池、字段注入、循环依赖全都不再是抽象名词而是我自己一行行调出来的真实代码。这篇文章就是我手写这个轻量级容器的完整复盘包含整体设计思路、核心注解定义、类扫描与注册实现、单例与多例Bean管理、依赖注入的递归处理以及循环依赖的二级缓存解决方案。无论你是面试前想快速吃透IoC原理还是想通过动手复现来加深对Spring的理解这篇文章都会给你一条可以直接照着写的路径。所有代码都是完整可运行的我尽量讲清楚每一步为什么这么做。1. 整体设计思路与组件拆解1.1 为什么明明有Spring还要自己手写一个容器手写一个简化版IoC容器最大的价值不是替代Spring而是通过最少的代码还原核心机制让你真正看懂“容器到底在干什么”。Spring本身的源码非常庞大光AbstractApplicationContext#refresh这一个方法就有十几个子步骤ClassPathXmlApplicationContext、AnnotationConfigApplicationContext、DefaultListableBeanFactory层层嵌套初学者看源码很容易在类与类之间的调用关系里丢失主线。而手写容器的过程本质上是给Spring做一次“降维”——只保留它最核心的三件事扫描Class文件、解析注解、维护Bean实例。写完之后你会形成一条清晰的主线容器就像一个“中介”你告诉它“有哪些类归你管”配置扫描路径它用反射把这些类找出来存成BeanDefinition然后按需实例化并且在实例化时把Bean之间的依赖关系一并处理掉。这个过程说起来简单每一步落地都有细节尤其是“A依赖B、B又依赖A”的循环依赖场景最容易暴露设计上的漏洞。1.2 注解负责“声明”反射负责“执行”这个容器的两把钥匙是注解和反射它们的分工非常明确。注解是用来做“标记”的。在你的业务代码里你没法告诉容器“这个类是Bean”除非容器提供一种扫描和识别机制。Spring选择的方式就是注解——Component标记一个类需要被容器托管Autowired标记一个字段需要注入依赖ComponentScan告诉容器从哪里开始扫描。这些注解本身不包含任何逻辑它们只是“贴在类上的标签”。真正干活的其实是反射。容器在运行时读取这些标签然后通过Class.forName()加载类、通过getDeclaredFields()拿到类的字段、通过getAnnotation()判断一个字段是否被Autowired标注最后通过setAccessible(true)绕过访问权限用field.set()完成赋值。这一整套流程Spring内部也是这么做的只不过它封装得更加完善加入了缓存、代理、各种扩展点。我用一个生活化的类比来理解注解是“报名表”你往上面填信息反射是“人力系统”它读取报名表核实信息给你分配工位和资源。没有反射注解就是一堆躺在源码里的死注释没有任何运行时能力。1.3 代码结构规划我的项目结构很清晰完全参照Spring的核心模块做了精简。整个项目的包名设计如下com.example.demo ├── annotation │ ├── Component.java │ ├── ComponentScan.java │ └── Autowired.java ├── container │ ├── BeanDefinition.java │ ├── AnnotationConfigApplicationContext.java │ └── DefaultSingletonBeanRegistry.java ├── service │ ├── UserService.java │ └── OrderService.java ├── controller │ └── UserController.java ├── test │ └── ApplicationTest.java └── Application.java容器部分的核心类是AnnotationConfigApplicationContext它做了三件关键事情读取启动类上的ComponentScan注解拿到扫描的基础包路径。遍历包路径下的所有Class文件筛选出标记了Component的类包装成BeanDefinition存入Map。对外提供getBean()方法在获取时完成实例化、依赖注入和单例缓存。包路径扫描是手写容器里比较有意思的一环它不是直接用Spring那套ClassPathScanningCandidateComponentProvider而是自己写一个基于ClassLoader的扫描器下面我会展开讲。2. 核心细节解析与实操要点2.1 三个核心注解的选择与定义定义注解是这个项目里最简单也最基础的一步。我给容器设计了三个注解刚好复刻Spring最常用的三个Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface Component { String value() default ; }Component用于类上标记该类需要被容器管理。它的value()属性允许指定Bean名称留空时默认用类名首字母小写作为名称。Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface ComponentScan { String value() default ; }ComponentScan用于配置类上指定扫描的基础包路径。Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface Autowired { }Autowired用于字段上标记容器需要自动注入该字段对应的Bean。注意Retention必须设为RUNTIME这样才能让反射在JVM运行时读取到注解信息。很多初学者搞不清楚这一点如果只写Retention(SOURCE)编译器处理完就丢弃了反射永远也看不见。这三个注解在真实开发中可以扩展出很多变种比如给Component增加scope属性让它支持原型模式给Autowired增加required属性让它支持可选注入。我的第一版只保留这些基础能力目的是不把核心逻辑混淆在繁琐的配置里。2.2 包扫描不是“扫文件”是“读类加载器的URL”这是整个手写容器里我认为最值得拿出来讲的部分。Spring的包扫描看起来玄乎本质上就是“把指定包路径下的.class文件找出来然后通过反射加载成Class对象”。核心的实现思路如下public class ClassPathScanner { public static SetClass? scan(String basePackage) { SetClass? classes new HashSet(); String path basePackage.replace(., /); try { EnumerationURL resources Thread.currentThread().getContextClassLoader() .getResources(path); while (resources.hasMoreElements()) { URL resource resources.nextElement(); File file new File(resource.toURI()); for (File classFile : file.listFiles()) { if (classFile.getName().endsWith(.class)) { String className classFile.getName().substring(0, classFile.getName().length() - 6); Class? clazz Class.forName( basePackage . className); classes.add(clazz); } } } } catch (Exception e) { throw new RuntimeException(包扫描失败: basePackage, e); } return classes; } }这里面有几个隐藏很深的细节。第一getResources(path)拿到的是URL枚举在IDE中运行对应的是本地的file协议路径。如果你在Jar包环境运行协议变成jar处理方式又不一样。所以这个扫描器在打成Fat Jar后通常会失效实际产品中要引入更健壮的扫描策略这就触及了Spring为什么包含那么多资源解析器的原因。第二我在第一版代码里漏掉了一个关键步骤需要判断resource.getProtocol()直接new File(resource.toURI())在jar场景下会直接报错。这也是为什么我说手写容器一定要搭一个带依赖管理的Maven工程方便在IDE里测试因为直接打包运行会遇到各种ClassLoader路径问题。第三这段扫描代码是一个“流程骨架”真正判断一个类是否应该交给容器的逻辑在下面的createBeanDefinitions里做。2.3 从Class对象到BeanDefinition把“元信息”存起来拿到所有Class对象之后下一步就是筛选和注册。这里我引入了BeanDefinition的概念这可能是理解Spring IoC最重要的一个概念。public class BeanDefinition { private Class? clazz; private String beanName; private String scope; // singleton 或 prototype private boolean lazy; // 省略getter/setter }容器内部有两个核心Mapprivate final MapString, BeanDefinition beanDefinitionMap new ConcurrentHashMap(); private final MapString, Object singletonObjects new ConcurrentHashMap();beanDefinitionMap存的是“配方”告诉容器这个Bean的类型是什么、名称是什么、是单例还是原型。singletonObjects存的是“成品”也就是已经创建好的单例对象。注册过程如下for (Class? clazz : classes) { if (!clazz.isAnnotationPresent(Component.class)) { continue; } Component component clazz.getAnnotation(Component.class); String beanName component.value(); if (beanName null || beanName.isEmpty()) { beanName lowerFirst(clazz.getSimpleName()); } BeanDefinition bd new BeanDefinition(); bd.setClazz(clazz); bd.setBeanName(beanName); bd.setScope(clazz.isAnnotationPresent(Scope.class) ? clazz.getAnnotation(Scope.class).value() : singleton); beanDefinitionMap.put(beanName, bd); }这里有个很容易忽视的问题为什么Spring要先把Bean定义信息存起来而不是直接实例化存对象答案在于Bean之间的依赖关系可能是“先定义的Bean依赖后定义的Bean”。如果边扫边实例化扫描到UserService时OrderService还没被定义UserService想注入OrderService就会失败。所以Spring才设计了“先收集定义、后创建对象”的两阶段模型。这个刻意分层的思想是我在手写之后才真正体会到的。2.4 单例池与工厂方法的取舍核心容器类的getBean()方法采用了“单例池 工厂方法”的组合模式public Object getBean(String beanName) { BeanDefinition bd beanDefinitionMap.get(beanName); if (bd null) { throw new NoSuchBeanDefinitionException(找不到Bean定义: beanName); } if (singleton.equals(bd.getScope())) { // 三级缓存的核心入口这里先写一级 Object singleton singletonObjects.get(beanName); if (singleton null) { synchronized (singletonObjects) { singleton singletonObjects.get(beanName); if (singleton null) { singleton createBean(bd); singletonObjects.put(beanName, singleton); } } } return singleton; } // 原型模式每次都创建新对象 return createBean(bd); }这里使用了双重检查锁因为容器的getBean()在多线程场景下可能被同时调用如果不需要保证单例的严格性第一版写一个简单的if (singleton null)也能跑但加上双重检查会让逻辑更接近真实的生产模式。Singleton模式下同一个Bean在全应用中只有一个实例这是Spring容器最重要的内存和状态管理机制。3. 实操过程与核心环节实现3.1 实例化与字段注入的完整流程我把容器从“拿到Bean定义”到“返回可用Bean”的完整流程整理成了一条链根据BeanDefinition中的Class对象通过clazz.getDeclaredConstructor().newInstance()创建一个原始对象注意这里还没有字段值是一个“半成品”。遍历该Class的所有字段对于标记了Autowired的字段调用getBean(fieldType)获取依赖Bean。通过field.setAccessible(true)和field.set(bean, dependency)完成依赖注入。返回注入完成的“准成品”。下面是createBean方法的核心代码private Object createBean(BeanDefinition bd) { Class? clazz bd.getClazz(); try { Object instance clazz.getDeclaredConstructor().newInstance(); // 第一步遍历字段填充依赖 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { Object dependency getBeanByType(field.getType()); field.setAccessible(true); field.set(instance, dependency); } } return instance; } catch (Exception e) { throw new RuntimeException(创建Bean失败: clazz.getName(), e); } } private Object getBeanByType(Class? type) { for (BeanDefinition bd : beanDefinitionMap.values()) { if (type.isAssignableFrom(bd.getClazz())) { return getBean(bd.getBeanName()); } } throw new NoSuchBeanDefinitionException(找不到类型: type.getName()); }这里按类型注入是一种简化方式。如果多个类都实现了同一个接口按类型注入就会产生歧义Spring面对这种情况会优先按字段名匹配匹配不上会报NoUniqueBeanDefinitionException。我的第一版实现就踩了这个坑后来加了一段按字段名匹配的逻辑String fieldName field.getName(); for (BeanDefinition bd : beanDefinitionMap.values()) { if (fieldName.equals(bd.getBeanName())) { // 命中注入 } }这段逻辑虽然朴素但是非常实用——它反映了一个真实场景下必须考虑的问题接口多实现时容器怎么选择注入目标。3.2 单例Bean实例化顺序的推演容器在启动后并不是立刻创建所有Bean而是“懒创建”到了getBean()才处理。但Spring的refresh()方法末尾会调用finishBeanFactoryInitialization()强制实例化所有非懒加载的单例Bean这一步叫“预实例化”。我手写的容器也在构造函数里加了这一步把所有单例Bean提前创建好public AnnotationConfigApplicationContext(Class? configClass) { // 1. 扫描 ComponentScan scan configClass.getAnnotation(ComponentScan.class); String basePackage scan.value(); SetClass? classes ClassPathScanner.scan(basePackage); // 2. 注册BeanDefinition registerBeanDefinitions(classes); // 3. 预实例化单例Bean for (String beanName : beanDefinitionMap.keySet()) { BeanDefinition bd beanDefinitionMap.get(beanName); if (singleton.equals(bd.getScope())) { getBean(beanName); } } }预实例化的好处很明显应用启动阶段就发现Bean创建和注入的错误而不是等到运行时报空指针。这也是为什么在实际业务中一个Bean的构造抛异常时SpringBoot应用会直接启动失败。容器的设计原则是“尽早暴露问题”好过运行到一半才爆炸。3.3 原型Bean与作用域扩展虽然第一版只支持单例但把原型Bean加上也很简单。我加了一个Scope注解支持singleton和prototype两种模式。区别只在于getBean()中如果是prototype就直接走createBean不进单例池。实际开发当中有一些状态型组件比如保存用户名和会话信息就不能用单例否则并发场景下的数据会串。Spring默认是单例这一点有争议但确实是最适合大多数无状态Bean的模式。手写这个容器之后你会发现作用域的决策并不是“配置上的小事”它直接影响线程安全性和内存占用。4. 从零手写三级缓存解决循环依赖4.1 循环依赖为什么会导致“死循环”循环依赖的场景非常简单UserService里注入了OrderServiceOrderService里又注入了UserService。在我还没有引入缓存之前整个流程是这样的用户调用getBean(userService)。容器创建UserService的原始对象A。发现A依赖OrderService于是调用getBean(orderService)。容器创建OrderService的原始对象B。发现B依赖UserService于是调用getBean(userService)。回到第2步无限循环下去最终栈溢出。实际上代码运行到第5步时第二次getBean(userService)在singletonObjects里找不到已创建的单例于是又重新走了一遍创建流程再次触发getBean(orderService)形成典型的递归死循环。解决这个问题的核心思路就是把“创建中的半成品”提前暴露出去让其他Bean可以先拿着这个半成品用等它把依赖填完再放到成品区。4.2 二级缓存怎么解决循环依赖Spring官方文档提到了三级缓存但实际上解决循环依赖最核心的是二级缓存就够了。三级缓存的第三级是为了处理AOP代理对象不是循环依赖的必要条件。我手写容器第一版只用了两级缓存也顺利解决了循环依赖。第一级singletonObjects存放已经创建完成的成品Bean。第二级earlySingletonObjects存放提前暴露的半成品Bean也就是已经实例化但还没完成依赖注入的对象。当容器发现需要提前暴露时会把这个半成品对象放入earlySingletonObjects。后续其他Bean来获取它时直接从二级缓存中拿到这个半成品完成自己的字段注入。等到半成品自己把依赖填完后容器再把它从二级缓存挪到一级缓存。来看我实现的核心代码private final MapString, Object singletonObjects new ConcurrentHashMap(); private final MapString, Object earlySingletonObjects new ConcurrentHashMap(); private final SetString singletonCurrentlyInCreation new HashSet(); public Object getBean(String beanName) { // 先从一级缓存拿拿不到再从二级缓存拿 Object bean singletonObjects.get(beanName); if (bean null) { bean earlySingletonObjects.get(beanName); } if (bean ! null) { return bean; } // 没有缓存开始创建 BeanDefinition bd beanDefinitionMap.get(beanName); return doCreateBean(beanName, bd); } private Object doCreateBean(String beanName, BeanDefinition bd) { Object instance null; try { instance bd.getClazz().getDeclaredConstructor().newInstance(); // 关键一步创建好实例后立刻放入二级缓存 synchronized (earlySingletonObjects) { earlySingletonObjects.put(beanName, instance); } // 进行依赖注入 for (Field field : instance.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { String dependencyName lowerFirst(field.getType().getSimpleName()); Object dependency getBean(dependencyName); field.setAccessible(true); field.set(instance, dependency); } } // 依赖注入完成从二级缓存移除放入一级缓存 earlySingletonObjects.remove(beanName); singletonObjects.put(beanName, instance); return instance; } catch (Exception e) { throw new RuntimeException(创建Bean失败: bd.getClazz().getName(), e); } }这段代码的关键点非常清楚在给当前Bean做字段注入之前先把它自己放到二级缓存。这样当它依赖的Bean反过来再次获取它时就能命中二级缓存拿到这个“不完整但已实例化”的对象从而中断递归链条。整个过程推演一下getBean(userService)创建半成品A放入二级缓存发现依赖orderService。getBean(orderService)创建半成品B放入二级缓存发现依赖userService。getBean(userService)一级缓存没有二级缓存命中A直接返回A。B的字段注入完成移除二级缓存的B放入一级缓存。回到步骤1的上下文A拿到B完成注入移除二级缓存的A放入一级缓存。循环依赖在3步就中断了。这个过程中的核心思想其实就是“不要等一个Bean完全创建好才给别人用先交出来一个半成品用完了再把成品换回去”。4.3 为什么Spring还要第三级缓存既然二级缓存能解决循环依赖Spring为什么还要第三级这一点我最初也想不明白后来结合AOP才理清。Spring里的一个Bean可能不是普通的Java对象而是被AOP增强过的代理对象。比如一个Service方法加了Transactional注解容器里存的实际上是一个代理对象。如果一个Bean既有循环依赖、又被AOP增强二级缓存里直接放普通半成品就有问题其他地方拿到的这个Bean没有被代理而最终一级缓存里的Bean是代理对象前后不一致AOP功能就失效了。第三级缓存singletonFactories里存的是ObjectFactory它是一个函数式接口在需要暴露半成品时才执行返回一个“可能经过代理的半成品”。这样既能解决循环依赖又能保证代理对象的一致性。手写项目的升级版里我也模拟了第三级缓存结构如下private final MapString, Object singletonObjects new ConcurrentHashMap(); private final MapString, Object earlySingletonObjects new ConcurrentHashMap(); private final MapString, ObjectFactory? singletonFactories new ConcurrentHashMap();getSingleton的逻辑变成先看一级再看二级最后从三级缓存中获取ObjectFactory执行getObject()得到半成品放入二级缓存删除三级缓存。这里我不再贴完整代码因为核心逻辑和二级缓存差不太多差别只在三级缓存存的是一个“懒执行”的工厂它可以在返回对象之前生成代理。对绝大部分学习场景来说理解二级缓存已经解决了90%的问题。5. 容器测试看看代码到底能不能跑5.1 准备两个互相依赖的Service为了验证循环依赖处理我造了一对互相引用的ServiceComponent public class UserService { Autowired private OrderService orderService; public void sayHello() { System.out.println(UserService 成功拿到了 OrderService: orderService); } } Component public class OrderService { Autowired private UserService userService; public void sayHello() { System.out.println(OrderService 成功拿到了 UserService: userService); } }5.2 启动容器并执行启动类代码如下ComponentScan(com.example.demo) public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(Application.class); UserService userService (UserService) context.getBean(userService); OrderService orderService (OrderService) context.getBean(orderService); userService.sayHello(); orderService.sayHello(); System.out.println(userService 下的 orderService 是否等于容器中的 orderService: (userService.getOrderService() orderService)); System.out.println(orderService 下的 userService 是否等于容器中的 userService: (orderService.getUserService() userService)); } }运行结果正常输出两个Service互相拿到对方对象最后两个比对结果都是true。这说明注入的对象确实是容器中的单例而不是新创建的独立对象。这个结果验证的不仅是“容器能跑”更是“依赖注入保持了单例一致性”。如果二级缓存写错位置上面的比对结果会变成false说明容器创建了多个不同的UserService实例单例的语义就被破坏了。5.3 加一个Controller验证Web层可用我还写了一个简化版Controller手动模拟Web层调用Component public class UserController { Autowired private UserService userService; public void handleRequest() { userService.sayHello(); } }测试代码里通过容器拿到userController调用handleRequest()控制台输出完整。这个简单的例子很好地演示了SpringBoot里三层架构怎么通过容器串起来Controller依赖ServiceService依赖其他Service或Repository所有依赖关系都交给容器处理业务类之间不再出现new关键字。6. 常见问题与排查技巧实录6.1 常见运行时异常速查表整个手写过程肯定会遇到不少坑我把最常见的几个问题整理成一个速查表异常现象可能原因排查思路ClassNotFoundException包扫描路径写错或类名拼接异常检查ComponentScan的value值确认路径包含目标包检查类名首字母小写规则是否与Bean名称一致NoSuchBeanDefinitionException类没加Component注解或包路径扫描范围不对确认类确实有注解在扫描代码里加日志打印确认Class是否被扫描到注入的字段为null字段没加Autowired或注入逻辑执行顺序不对检查字段注解是否存在确认容器调用field.set()之前是否拿到正确依赖对象InstantiationException类没有无参构造函数给类补一个public无参构造器或者改用clazz.getDeclaredConstructor()并处理参数StackOverflowError循环依赖没有二级缓存保护检查doCreateBean是否在最开始就把实例放入了earlySingletonObjects注入时出现IllegalAccessException没有调用field.setAccessible(true)反射调用private字段前必须开启权限6.2 排查注入为null的完整步骤有一次我测试时发现Service里的属性怎么都是null找了半天最后发现是包扫描只覆盖了com.example.demo.annotation没有覆盖com.example.demo.service。这种问题的排查路径非常有代表性第一步在扫描器里打印所有扫描到的类明确到底有没有拿到目标Class。第二步在registerBeanDefinitions里打印Bean的名称列表确认Bean定义有没有成功注册。第三步在getBean方法里增加断点观察BeanName和BeanDefinition的对应关系。第四步在field.set之前打印依赖对象是否为null。我建议手写容器时一定要预留这种“日志埋点”的思维它能帮你快速定位问题到底出在扫描、定义、实例化还是注入哪个环节。Spring框架本身为什么日志体系这么完善就是因为这种分布式排查经验太重要了。6.3 手写容器过程中的项目配置与测试建议我用的是Maven项目Java 8代码量不大但必须保证pom.xml里没有额外引入Spring依赖否则可能因为类名冲突干扰调试。测试时直接在main方法里跑不依赖JUnit这样能够最快看到输出。有一点我想特别提一下不要一开始就追求完美非要支持接口注入、构造器注入、BeanPostProcessor这些特性。先做一个只支持字段注入的版本跑通基本流程再逐步加功能。这个项目的核心价值在于理解容器的“骨架”当你把骨架搭对了后面加功能都是体力活。7. 版本升级路径从手写基础容器到更接近Spring基础版本的容器能跑通之后我做了几个非常有价值的升级这些升级让我对Spring的理解又深了一层。第一个升级是支持BeanPostProcessor。Spring里很多功能比如AOP代理、属性填充、Resource处理都是通过BeanPostProcessor这个钩子接口实现的。我仿照Spring定义了一个public interface BeanPostProcessor { default Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; } default Object postProcessAfterInitialization(Object bean, String beanName) { return bean; } }在createBean的依赖注入完成之后、放入单例池之前遍历所有注册的BeanPostProcessor调用这两个方法。这个扩展点的价值在于它把“创建Bean的流程”和“增强Bean的流程”解耦了。AOP增强、代理生成都是通过重写postProcessAfterInitialization方法来实现的不需要改动容器创建Bean的核心逻辑。第二个升级是支持更完善的Bean生命周期回调。Spring里Bean创建过后如果实现了InitializingBean接口会调用afterPropertiesSet()如果配置了init-method也会被调用。我加入了一个简单的判断如果Bean实现了某个Callback接口则反射调用对应方法。这让我真正理解了为什么PostConstruct注解能在构造后执行。第三个升级是容器的事件机制。这个相对边缘但能体现Spring“容器不只是放对象还管协作”的思想。我加了一个最简版本的事件发布器容器通过ApplicationEventPublisher发布事件监听器通过监听注解接收事件。升级之后我对EventListener底层的实现逻辑也有了更加具体的认知。写在最后的一点实践经验手写一遍IoC容器比我读十遍源码更有收获。我最大的感受是Spring里的很多设计单独看代码觉得复杂但当你自己从零开始写一遍就会发现它的每一处设计都是“被逼出来的”。二级缓存是为了解决循环依赖BeanDefinition是为了解耦“类扫描”和“对象创建”BeanPostProcessor是为了扩展而不修改这些设计都是遇到了真实问题之后自然生长出来的解决方案。这个项目后续还可以扩展的方向很多比如实现Value注解读取配置文件、实现Configuration标记配置类、支持构造器注入、支持FactoryBean等等。我建议有Java基础的读者都动手写一遍你会发现面试时那些“背”下来的原理其实都是可以推理出来的常识。如果在写的过程中遇到卡壳的地方欢迎随时交流我会尽力帮你排查。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进