行业资讯
手写Spring框架核心:从IoC容器到依赖注入的实现原理
1. 从零开始为什么我们要手写一个Spring框架如果你是一名Java开发者那么“Spring”这个名字对你来说可能就像空气一样无处不在。从企业级的后端服务到个人练手的小项目Spring框架几乎成了Java Web开发的代名词。但不知道你有没有过这样的感觉每天用着Autowired、Controller这些注解配置着application.yml虽然功能实现了心里却总有点不踏实。这些注解背后到底发生了什么ApplicationContext启动时那一连串的日志背后框架究竟为我们做了哪些“脏活累活”这就是我决定动手写一个简化版Spring框架的初衷。不是为了替代Spring那无异于螳臂当车。真正的目的是**“拆解”与“理解”**。通过亲手实现核心流程你能穿透那些被封装得极好的API直击IoC控制反转和AOP面向切面编程的设计精髓。这个过程远比读十篇源码分析文章来得深刻。你会发现很多看似高深的概念其底层思想往往简洁而优美。当你再回到日常开发中面对复杂的Bean循环依赖报错、AOP拦截失效等问题时你的脑海里会自然浮现出框架内部的运作图景排查问题的思路将无比清晰。所以这篇内容不是一份生产级框架的构建指南而是一份**“深度解剖实验报告”**。我们将聚焦于Spring最核心的两大功能IoC容器和基于注解的依赖注入。我会带你从零开始用最纯粹的Java代码一步步搭建出一个能跑起来的迷你Spring。在这个过程中你会亲手定义注解、扫描类路径、创建并管理Bean、处理依赖关系。相信我当你看到自己写的MyAutowired注解成功将一个Bean注入到另一个Bean中时那种豁然开朗的成就感是无与伦比的。2. 核心设计蓝图我们的迷你Spring要做什么在动手写代码之前我们必须先画好蓝图。一个完整的Spring框架庞大而复杂但我们只需要抓住其灵魂。我们的目标是实现一个极度精简但五脏俱全的版本它需要完成以下核心任务模仿注解驱动开发定义我们自己的注解例如MyComponent类比Component、MyAutowired类比Autowired让框架能够识别这些注解。实现Bean容器IoC容器这是一个最核心的容器负责托管所有被MyComponent标记的类实例。它的核心职责是扫描与发现在指定的包路径下扫描所有类文件找出被注解标记的类。实例化与缓存创建这些类的对象即Bean并将它们缓存起来确保每个Bean在容器中是单例的。依赖注入分析Bean之间的依赖关系通过MyAutowired注解标识并自动将依赖的Bean实例“注入”到目标Bean的属性中。提供容器入口提供一个类似ApplicationContext的启动类用户通过它来初始化容器并从中获取完全装配好的Bean。这个设计剥离了Spring中诸如AOP、事务管理、MVC等高级特性仅仅保留了IoC最本质的流程注册 - 实例化 - 装配。实现这个流程我们就抓住了Spring的“七寸”。2.1 技术选型与准备工作我们不需要任何额外的第三方库纯粹使用Java标准库JDK 8即可来完成。这能让我们更专注于逻辑本身。主要用到的Java技术包括反射Reflection这是整个框架的基石。用于在运行时获取类的注解信息、创建实例、设置私有字段的值。注解Annotation定义我们自己的元数据。类路径扫描使用ClassLoader和文件IO来遍历包路径下的类文件。集合框架使用ConcurrentHashMap等来充当Bean容器保证线程安全。注意在生产环境中Spring使用了更高效的字节码操作如CGLIB、ASM和复杂的缓存机制。我们为了理解原理全部采用最直观的反射实现这会导致性能不高但绝对足够清晰。首先创建一个新的Maven或Gradle项目或者就是一个简单的Java项目目录。我建议的包结构如下这能让代码逻辑更清晰my-spring-core ├── src/main/java │ └── com │ └── myframework │ ├── annotation │ │ ├── MyComponent.java │ │ └── MyAutowired.java │ ├── context │ │ └── MyApplicationContext.java │ ├── core │ │ └── BeanContainer.java │ └── util │ └── ClassScanner.java └── pom.xml (或build.gradle)3. 打造基石自定义注解的定义框架的行为由注解来驱动。我们先来定义两个最基础的注解。MyComponent注解用于标记一个类是需要被IoC容器管理的Bean。package com.myframework.annotation; import java.lang.annotation.*; /** * 模仿 Component用于标记一个类为Spring Bean。 */ Target(ElementType.TYPE) // 表示该注解可以用于类、接口、枚举上 Retention(RetentionPolicy.RUNTIME) // 至关重要表示注解信息在运行时保留这样反射才能获取到 public interface MyComponent { /** * 指定Bean在容器中的名称默认为类名首字母小写。 */ String value() default ; }MyAutowired注解用于标记需要自动注入的字段。package com.myframework.annotation; import java.lang.annotation.*; /** * 模仿 Autowired用于标记需要自动注入的字段。 */ Target(ElementType.FIELD) // 表示该注解只能用于字段上 Retention(RetentionPolicy.RUNTIME) public interface MyAutowired { // 简化版不需要额外属性 }这两个注解的定义非常简单但Retention(RetentionPolicy.RUNTIME)是灵魂所在。它告诉Java编译器这个注解的信息不仅要保留在编译后的.class文件里还要在程序运行时可以通过反射读取到。如果没有这个元注解我们的框架在运行时将无法感知到任何标记整个流程就无法启动。4. 容器核心BeanContainer的实现BeanContainer是我们框架的心脏它用一个Map来存储所有Bean实例并提供了注册和获取Bean的核心方法。这里我们设计它为单例。package com.myframework.core; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Bean容器负责存储和管理所有Bean实例。 */ public class BeanContainer { // 单例实例 private static volatile BeanContainer instance; // Bean缓存池Key为Bean的名称或类全限定名Value为Bean实例 private final MapString, Object beanMap new ConcurrentHashMap(); private BeanContainer() { // 私有构造器 } /** * 获取容器单例DCL双检锁实现保证线程安全。 */ public static BeanContainer getInstance() { if (instance null) { synchronized (BeanContainer.class) { if (instance null) { instance new BeanContainer(); } } } return instance; } /** * 将Bean实例注册到容器中。 * param beanName Bean的名称 * param bean Bean的实例对象 */ public void registerBean(String beanName, Object bean) { if (beanName ! null bean ! null) { beanMap.put(beanName, bean); } } /** * 根据名称从容器中获取Bean。 * param beanName Bean的名称 * return Bean实例如果不存在则返回null */ public Object getBean(String beanName) { return beanMap.get(beanName); } /** * 根据类型从容器中获取Bean简化版返回第一个匹配类型的Bean。 * 这里存在一个隐患如果同一类型有多个Bean此方法无法区分。 * Spring是通过结合Qualifier注解或Bean名称来解决的。 */ public T T getBean(ClassT clazz) { return beanMap.values().stream() .filter(clazz::isInstance) .map(clazz::cast) .findFirst() .orElse(null); } /** * 获取容器中所有Bean的名称。 */ public String[] getBeanDefinitionNames() { return beanMap.keySet().toArray(new String[0]); } /** * 获取容器中Bean的数量。 */ public int size() { return beanMap.size(); } }这个容器目前只提供了最基本的存储和查找功能。它还没有和注解扫描、依赖注入联动起来。但它是我们后续所有操作的基础数据存储。5. 类的侦探ClassScanner类路径扫描器框架需要知道去哪里找那些被MyComponent标记的类。ClassScanner就负责这个“侦探”工作。它的核心逻辑是将指定的包名如com.example.service转换为文件系统路径然后递归遍历该路径下的所有.class文件并使用类加载器加载它们最后过滤出带有目标注解的类。这里有一个关键细节在IDE中运行和将项目打包成JAR后运行类文件的存放位置是不同的文件系统 vs JAR包内。一个健壮的扫描器需要同时处理这两种情况。为了简化我们首先实现文件系统路径下的扫描这是理解原理的关键。package com.myframework.util; import com.myframework.annotation.MyComponent; import java.io.File; import java.net.URL; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; /** * 类扫描器用于扫描指定包下所有被MyComponent注解的类。 */ public class ClassScanner { /** * 扫描指定包及其子包下所有带有MyComponent注解的类。 * param packageName 包名如 com.example * return 符合条件的类的Class对象集合 */ public static SetClass? scanComponents(String packageName) { SetClass? classSet new HashSet(); // 1. 将包名转换为文件路径 String path packageName.replace(., /); ClassLoader classLoader Thread.currentThread().getContextClassLoader(); try { // 2. 获取资源枚举 EnumerationURL resources classLoader.getResources(path); while (resources.hasMoreElements()) { URL resource resources.nextElement(); String protocol resource.getProtocol(); // 3. 处理文件系统类型的资源开发环境常见 if (file.equals(protocol)) { String filePath resource.getFile(); findAndAddClassesInPackageByFile(packageName, filePath, classSet, classLoader); } // 4. 处理JAR文件类型的资源生产环境常见-- 此处为简化暂不实现 // else if (jar.equals(protocol)) { ... } } } catch (Exception e) { e.printStackTrace(); } return classSet; } /** * 在文件系统中递归查找.class文件并加载检查。 * param packageName 包名 * param packagePath 包对应的文件路径 * param classSet 结果集合 * param classLoader 类加载器 */ private static void findAndAddClassesInPackageByFile(String packageName, String packagePath, SetClass? classSet, ClassLoader classLoader) { File dir new File(packagePath); if (!dir.exists() || !dir.isDirectory()) { return; } // 列出目录下所有文件和子目录 File[] files dir.listFiles(file - file.isDirectory() || file.getName().endsWith(.class)); if (files null) { return; } for (File file : files) { if (file.isDirectory()) { // 递归扫描子目录 findAndAddClassesInPackageByFile(packageName . file.getName(), file.getAbsolutePath(), classSet, classLoader); } else { // 如果是.class文件去除后缀得到类名 String className file.getName().substring(0, file.getName().length() - 6); try { // 加载类 Class? clazz classLoader.loadClass(packageName . className); // 判断该类是否被MyComponent注解标记 if (clazz.isAnnotationPresent(MyComponent.class)) { classSet.add(clazz); } } catch (ClassNotFoundException e) { e.printStackTrace(); } } } } }这个扫描器虽然简陋但它清晰地展示了“扫描-加载-过滤”的核心步骤。在生产级Spring中这一步通常由更高效的ClassPathScanningCandidateComponentProvider等工具完成并考虑了复杂的排除过滤规则。6. 灵魂注入MyApplicationContext与依赖注入流程现在我们有了解析注解MyComponent、有容器BeanContainer、有扫描器ClassScanner。是时候把它们串联起来实现从启动到依赖注入的完整生命周期了。MyApplicationContext就是这个总指挥。它的工作流程可以分为清晰的四步扫描与注册扫描指定包找到所有Bean的Class定义并创建它们的实例此时还是“空壳”依赖未注入放入容器。依赖注入遍历容器中的所有Bean检查它们的字段如果字段被MyAutowired标记就从容器中找出对应的依赖Bean并注入。处理循环依赖简化这是一个难点我们实现一个基础版本。提供获取Bean的接口作为面向用户的API。6.1 初始化与Bean实例化我们先实现前两步。这里会遇到第一个实践坑点实例化顺序。如果A依赖BB也依赖A我们应该先实例化谁一个简单的策略是分两阶段进行第一阶段创建所有Bean的实例调用无参构造器此时它们的依赖字段都是null。先把这些“半成品”放入容器。第二阶段遍历所有Bean为它们的MyAutowired字段注入值。package com.myframework.context; import com.myframework.annotation.MyAutowired; import com.myframework.annotation.MyComponent; import com.myframework.core.BeanContainer; import com.myframework.util.ClassScanner; import java.lang.reflect.Field; import java.util.Set; /** * 模仿Spring的ApplicationContext是框架的启动入口和核心容器。 */ public class MyApplicationContext { private final BeanContainer beanContainer; // 需要扫描的根包路径 private final String basePackage; public MyApplicationContext(String basePackage) { this.beanContainer BeanContainer.getInstance(); this.basePackage basePackage; // 启动时自动刷新容器 refresh(); } /** * 刷新容器扫描 - 实例化 - 注入。 */ private void refresh() { // 1. 扫描包获取所有Component类 SetClass? componentClasses ClassScanner.scanComponents(basePackage); // 2. 创建所有Bean实例此时未注入依赖 createBeans(componentClasses); // 3. 注入依赖 autowireBeans(); } /** * 创建Bean实例并注册到容器。 */ private void createBeans(SetClass? classes) { for (Class? clazz : classes) { try { // 获取注解上定义的Bean名称如果没有则使用类名首字母小写 MyComponent annotation clazz.getAnnotation(MyComponent.class); String beanName annotation.value(); if (beanName null || beanName.isEmpty()) { // 简单规则将类名首字母转为小写如 UserService - userService beanName clazz.getSimpleName(); beanName beanName.substring(0, 1).toLowerCase() beanName.substring(1); } // 通过反射创建实例要求类必须有无参构造器 Object beanInstance clazz.getDeclaredConstructor().newInstance(); // 注册到容器 beanContainer.registerBean(beanName, beanInstance); // 同时也以类全限定名和类本身为Key注册一份方便按类型获取 beanContainer.registerBean(clazz.getName(), beanInstance); } catch (Exception e) { throw new RuntimeException(Failed to create bean for class: clazz.getName(), e); } } } /** * 为容器中所有Bean的MyAutowired字段注入依赖。 */ private void autowireBeans() { String[] beanNames beanContainer.getBeanDefinitionNames(); for (String beanName : beanNames) { Object bean beanContainer.getBean(beanName); // 注入依赖 injectDependencies(bean); } } /** * 为单个Bean注入依赖。 */ private void injectDependencies(Object bean) { Class? clazz bean.getClass(); // 遍历所有字段包括私有字段 for (Field field : clazz.getDeclaredFields()) { // 检查字段是否带有MyAutowired注解 if (field.isAnnotationPresent(MyAutowired.class)) { // 获取字段的类型即需要注入的Bean的类型 Class? fieldType field.getType(); // 根据类型从容器中查找Bean简化处理取第一个 Object dependency beanContainer.getBean(fieldType); if (dependency null) { throw new RuntimeException(No qualifying bean of type fieldType.getName() available for autowiring into field field.getName() of bean bean.getClass().getName() ); } try { // 设置字段可访问即使是private field.setAccessible(true); // 将依赖Bean注入到目标Bean的字段中 field.set(bean, dependency); } catch (IllegalAccessException e) { throw new RuntimeException(Failed to inject dependency for field: field.getName(), e); } } } } /** * 对外提供获取Bean的接口。 */ public Object getBean(String name) { return beanContainer.getBean(name); } public T T getBean(ClassT clazz) { return beanContainer.getBean(clazz); } }6.2 直面挑战循环依赖的简易处理方案上面的代码在大多数简单场景下可以工作但一旦遇到循环依赖比如AService依赖BService同时BService也依赖AService它就会崩溃。原因在于我们的注入逻辑是单线程顺序执行的当为AService注入BService时BService可能还未被创建如果扫描顺序导致AService先被处理或者BService虽然被创建了但它在等待注入AService时AService的注入过程又未完成形成了一个死锁。Spring通过三级缓存singletonObjectsearlySingletonObjectssingletonFactories来优雅地解决这个问题。其核心思想是提前暴露引用。我们来实现一个极度简化的版本理解其思想我们修改createBeans和autowireBeans流程引入“早期暴露”的概念。在createBeans阶段创建实例后立即将这个“半成品”属性未注入放入一个专门的“早期暴露缓存”比如一个MapString, Object。在injectDependencies时先从完整的容器里找如果找不到再去“早期暴露缓存”里找。这样当AService需要BService时BService可能还在创建中但它的引用已经存在于“早期暴露缓存”里AService就能拿到这个引用完成注入。后续BService的注入流程再去“早期暴露缓存”里拿到AService的引用。// 在MyApplicationContext中增加一个早期引用缓存 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(); private void createBeans(SetClass? classes) { for (Class? clazz : classes) { try { // ... 计算beanName ... Object beanInstance clazz.getDeclaredConstructor().newInstance(); // 关键步骤在注入依赖前先放入早期缓存 earlySingletonObjects.put(beanName, beanInstance); // 注册到正式容器此时依赖仍是null beanContainer.registerBean(beanName, beanInstance); beanContainer.registerBean(clazz.getName(), beanInstance); } catch (Exception e) { // ... 异常处理 ... } } } private void injectDependencies(Object bean) { Class? clazz bean.getClass(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MyAutowired.class)) { Class? fieldType field.getType(); // 修改获取依赖的逻辑先从容器的正式缓存找找不到再从早期缓存找 Object dependency beanContainer.getBean(fieldType); if (dependency null) { // 尝试从早期缓存中通过类型查找这里需要遍历是简化版的性能瓶颈 dependency earlySingletonObjects.values().stream() .filter(fieldType::isInstance) .findFirst() .orElse(null); } if (dependency null) { throw new RuntimeException(No bean found for type: fieldType.getName()); } try { field.setAccessible(true); field.set(bean, dependency); } catch (IllegalAccessException e) { // ... 异常处理 ... } } } }这个方案非常粗糙性能也差需要遍历早期缓存并且无法处理更复杂的代理对象循环依赖。但它清晰地演示了**“提前暴露引用”**这一解决循环依赖的核心思想。理解这一点再去阅读Spring的三级缓存源码就会顺畅很多。7. 实战检验编写测试用例框架写好了是骡子是马拉出来遛遛。我们创建几个测试类来模拟真实的使用场景。首先定义两个服务类并让它们相互依赖制造一个循环依赖的场景。UserService.javapackage com.example.service; import com.myframework.annotation.MyComponent; import com.myframework.annotation.MyAutowired; MyComponent // 标记为Bean public class UserService { MyAutowired // 需要注入OrderService private OrderService orderService; private String name UserService; public UserService() { System.out.println(UserService实例被创建); } public void sayHello() { System.out.println(Hello from name); if (orderService ! null) { System.out.println(我依赖的OrderService是: orderService); orderService.sayHello(); } else { System.out.println(OrderService 依赖注入失败); } } // Getter/Setter 省略... }OrderService.javapackage com.example.service; import com.myframework.annotation.MyComponent; import com.myframework.annotation.MyAutowired; MyComponent public class OrderService { MyAutowired // 需要注入UserService形成循环依赖 private UserService userService; private String name OrderService; public OrderService() { System.out.println(OrderService实例被创建); } public void sayHello() { System.out.println(Hello from name); if (userService ! null) { System.out.println(我依赖的UserService是: userService); } else { System.out.println(UserService 依赖注入失败); } } // Getter/Setter 省略... }一个独立的组件不参与循环依赖MyRepository.javapackage com.example.repository; import com.myframework.annotation.MyComponent; MyComponent(myRepo) // 指定Bean名称为myRepo public class MyRepository { public void query() { System.out.println(Executing query from MyRepository...); } }现在编写一个启动类来测试我们的迷你Spring框架Application.javapackage com.example; import com.example.service.UserService; import com.example.service.OrderService; import com.example.repository.MyRepository; import com.myframework.context.MyApplicationContext; public class Application { public static void main(String[] args) { System.out.println( 启动我的迷你Spring容器 ); // 初始化容器扫描com.example包及其子包 MyApplicationContext context new MyApplicationContext(com.example); System.out.println(\n 从容器中获取Bean并测试 ); // 1. 通过类型获取UserService UserService userService context.getBean(UserService.class); if (userService ! null) { userService.sayHello(); } // 2. 通过类型获取OrderService OrderService orderService context.getBean(OrderService.class); if (orderService ! null) { orderService.sayHello(); } // 3. 通过指定名称获取MyRepository MyRepository repo (MyRepository) context.getBean(myRepo); if (repo ! null) { repo.query(); } // 4. 验证单例获取的userService和orderService中注入的是否是同一个实例 System.out.println(\n 验证单例与循环依赖 ); System.out.println(UserService实例: userService); System.out.println(OrderService中注入的UserService: orderService.getUserService()); System.out.println(是否为同一实例: (userService orderService.getUserService())); System.out.println(\n 容器信息 ); System.out.println(容器中Bean数量: context.getBeanCount()); // 需要为MyApplicationContext添加此方法 } }运行这个main方法如果你的实现正确应该能看到类似以下的输出 启动我的迷你Spring容器 UserService实例被创建 OrderService实例被创建 MyRepository实例被创建 从容器中获取Bean并测试 Hello from UserService 我依赖的OrderService是: com.example.service.OrderServicexxxxxx Hello from OrderService 我依赖的UserService是: com.example.service.UserServiceyyyyyy Executing query from MyRepository... 验证单例与循环依赖 UserService实例: com.example.service.UserServiceyyyyyy OrderService中注入的UserService: com.example.service.UserServiceyyyyyy 是否为同一实例: true 容器信息 容器中Bean数量: 3看到UserService和OrderService成功相互注入并且是同一个实例就证明我们的迷你IoC容器和依赖注入机制包括对循环依赖的基础处理都成功了8. 避坑指南与深度思考通过这个手写项目我们不仅实现了功能更踩中了许多设计上的“坑”。这些经验对于理解Spring乃至任何框架设计都至关重要。8.1 遇到的典型问题与解决方案ClassNotFoundException或扫描不到类问题在ClassScanner中使用classLoader.loadClass()加载类时失败。排查检查packageName参数是否正确路径分隔符是否是.。检查类文件是否真的在编译输出目录如target/classes下。在IDE中运行时注意工作目录和类路径。使用classLoader.getResource()打印一下实际查找的路径。技巧在开发扫描器时可以先将classLoader.loadClass替换为Class.forName(className)试试后者会触发静态初始化块可能暴露更深层次的问题但有助于定位。依赖注入失败字段为null问题MyAutowired字段没有被赋值。排查首先确认字段是否真的是private且带有MyAutowired注解。在injectDependencies方法中打日志查看正在处理的Bean和要注入的字段类型。确认依赖的Bean是否已被成功注册到容器中。检查beanContainer的beanMap内容。最常见原因依赖的Bean没有被MyComponent注解标记或者它所在的包不在basePackage扫描范围内。循环依赖导致栈溢出或空指针问题在没有实现“早期暴露”机制前A和B相互依赖会导致无限递归或注入null。解决方案正如我们上面实现的简易“早期暴露缓存”其核心是将实例化与初始化分离。Spring的三级缓存是这一思想的工业级实现它还处理了AOP代理对象的特殊循环依赖情况。思考并非所有循环依赖都能解决。构造器注入Constructor Injection的循环依赖Spring默认也无法解决会抛出BeanCurrentlyInCreationException。这是因为在构造器调用时对象必须被完整创建无法提前暴露一个“半成品”。8.2 从迷你框架到真实Spring的鸿沟我们的玩具框架和真正的Spring相比缺失了太多关键特性理解这些缺失才能明白Spring的复杂性所在作用域Scope我们只实现了单例Singleton。Spring还有原型Prototype、请求Request、会话Session等作用域容器的管理逻辑要复杂得多。生命周期回调PostConstruct、PreDestroy、InitializingBean、DisposableBean等。容器需要在Bean属性注入后、销毁前执行特定逻辑。多种依赖注入方式我们只实现了字段注入。Spring还支持构造器注入官方推荐和Setter方法注入每种方式的处理逻辑不同。AOP面向切面编程这是Spring的另一大支柱。它通常通过动态代理JDK Proxy或CGLIB实现在Bean创建过程中可能需要生成代理对象这极大地增加了容器创建的复杂度也是三级缓存存在的重要原因之一。配置的多样性我们只支持注解配置。Spring还支持完整的XML配置、Java ConfigConfiguration以及各种外部化配置PropertySource。强大的Bean后处理器BeanPostProcessor这是Spring框架高度可扩展的秘诀。像Autowired、Resource、Value的处理以及AOP代理的创建都是通过内置的BeanPostProcessor实现的。我们的框架硬编码了注入逻辑而Spring将其设计成了可插拔的组件。资源管理、国际化、事件机制等这些构成了Spring庞大的生态系统。8.3 核心收获不仅仅是代码完成这个项目后当你再回到Spring的怀抱你看待Autowired、ApplicationContext、Bean生命周期的眼光会完全不同。注解不再是魔法你知道了Retention(RetentionPolicy.RUNTIME)是它们起效的前提框架在启动时通过反射读取这些信息来驱动行为。容器是中心化的Map你理解了ApplicationContext本质上是一个高级的、线程安全的对象工厂和缓存管理器。依赖注入是赋值操作你明白了所谓的“注入”就是框架通过反射找到对象的私有字段并调用field.set(bean, dependency)。循环依赖有解你了解了“提前暴露引用”是打破循环依赖僵局的关键钥匙。设计模式无处不在工厂模式BeanFactory、单例模式Bean容器、代理模式AOP、模板方法模式BeanPostProcessor等在Spring中得到了极致应用。这个手写的过程是一次彻底的去神秘化之旅。它让你从框架的“使用者”转变为“理解者”甚至初步的“设计者”。下次当你的Spring项目启动报出一个晦涩的Bean创建错误时你可能会会心一笑因为你的脑海里已经有一幅清晰的流程图知道该从哪个环节开始排查了。这就是动手实现带来的、最深层的价值。
郑州网站建设
网页设计
企业官网