从零手写注解实现一个简易的 Spring IoC 容器导语在 Java 开发中Spring 框架的注解如Component、Autowired几乎是每天都会打交道的工具。它们看似散发着神秘的魔法但底层的运转逻辑其实非常朴素。为了拆解这一机制最好的办法就是自己动手写一个简易版的 Spring 容器。本文将不依赖任何第三方框架仅用 Java 原生的注解和反射带大家一步步构建一个拥有自动包扫描、Bean 自动注册功能的迷你 IoC 容器。一、前置基础静态对象new与 动态反射 的本质区别在动手写代码前我们必须先厘清一个底层问题我们平时用 new 创建对象和用动态反射创建对象到底有什么区别特性静态对象创建new动态反射创建Reflection决定时期编译期Compile-time写代码时就已经定死了类型。运行期Runtime程序运行的时候才去决定创建什么类型。耦合度强耦合。如果类不存在项目直接报错无法通过编译。极低耦合。写代码时只需写一个占位符如字符串类不存在也能编译成功。语法实现UserService service new UserService();Class.forName(org.example.UserService).newInstance();底层依据依赖程序员在源码里写死的类型。依赖运行时的配置信息如配置文件、注解、用户输入。应用场景适用于日常业务开发中类型固定、关系明确的场景。适用于通用框架开发用于解耦和批量对象管理。为什么手写 Spring 不能用 new因为 Spring 框架在多年前编写、打包并发布时我们今天写的 UserService 类甚至都还没有被创建出来。如果框架里用 new代码就得写死publicObjectgetBean(){returnnewUserService();// 无法通过编译因为框架不知道 UserService 是什么}因此框架必须采用动态反射。框架只保留一条通用的对象创建流水线在运行期动态地将类加载并实例化publicObjectgetBean(StringclassName){// 只要运行期传来类名不管是什么类都能动态创建出来returnClass.forName(className).getDeclaredConstructor().newInstance();}二、疑惑直接反射不香吗注解到底解决了什么问题既然用反射可以直接创建对象那为什么还要定义注解、贴注解、判断注解如果我们的代码是这样写的Class?clazzClass.forName(org.example.UserService);if(clazz.isAnnotationPresent(MyComponent.class)){Objectinstanceclazz.getDeclaredConstructor().newInstance();}此时因为类名org.example.UserService依然是硬编码写死在代码里的注解确实形同鸡肋。但当反射遇上批量自动化扫描时注解就成了唯一的通行证和过滤器。在一个真实的项目中一个文件夹下存在各种各样的类UserService需要容器管理的业务类User普通数据实体类不需要创建单例StringUtils工具类不需要实例化当程序自动遍历该文件夹下所有的 .class 文件时它面对的是一组未知的类群。程序如何知道哪些类该实例化哪些该跳过此时注解就起到了筛选的作用。程序逐一检查“你贴了 MyComponent 标签吗”贴了就用反射实例化没贴就直接忽略。三、手把手编码实现下面我们开始用 4 个步骤正式构建这个自动化容器。第一步定义自定义注解 MyComponent在 org.example 包下创建一个 Annotation 类命名为 MyComponentpackageorg.example;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;// 限制该注解只能用在类上Target(ElementType.TYPE)// 确保注解在运行期Runtime依然保留以便反射机制读取Retention(RetentionPolicy.RUNTIME)publicinterfaceMyComponent{// 允许给 Bean 起一个名字默认是空字符串Stringvalue()default;}第二步编写业务类并贴上标签我们编写两个普通的业务类并用 MyComponent 贴上标签。1. 创建 UserService.javapackageorg.example;MyComponent(userService)// 贴上标签命名为 userServicepublicclassUserService{publicUserService(){System.out.println( UserService 对象已被创建);}publicvoidsayHello(Stringname){System.out.println(Hello, name这是来自 UserService 的问候。);}}2. 创建 OrderService.javapackageorg.example;MyComponent(orderService)// 贴上标签命名为 orderServicepublicclassOrderService{publicOrderService(){System.out.println( OrderService 对象已被创建);}publicvoidprintOrder(){System.out.println(订单模块您有一笔新的订单已生成。);}}第三步编写核心容器 MyApplicationContext在 org.example 下创建 MyApplicationContext.java。它是我们手写框架的核心负责自动扫描包路径、过滤出带有 MyComponent 的类并用反射进行批量实例化。packageorg.example;importjava.io.File;importjava.net.URL;importjava.net.URLDecoder;importjava.nio.charset.StandardCharsets;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;publicclassMyApplicationContext{// 单例池用于存储被容器管理的 Bean 实例K: Bean名称, V: 实例对象privatefinalMapString,ObjectsingletonObjectsnewConcurrentHashMap();publicMyApplicationContext(StringpackageName){try{// 1. 将包名如 org.example转换为相对路径如 org/exampleStringpackagePathpackageName.replace(.,/);// 2. 利用类加载器ClassLoader获取该包在磁盘上的绝对地址ClassLoaderclassLoaderThread.currentThread().getContextClassLoader();URLurlclassLoader.getResource(packagePath);if(urlnull){System.out.println(未找到指定的包路径);return;}// 对路径进行解码防止路径中包含空格等特殊字符导致解析失败StringfolderPathURLDecoder.decode(url.getFile(),StandardCharsets.UTF_8);FilefoldernewFile(folderPath);// 3. 遍历这个文件夹下的所有文件if(folder.exists()folder.isDirectory()){File[]filesfolder.listFiles();if(files!null){for(Filefile:files){// 我们只处理以 .class 结尾的字节码文件if(file.getName().endsWith(.class)){// 4. 将文件名还原为完整的类名// 例如 UserService.class - org.example.UserServiceStringclassNamepackageName.file.getName().substring(0,file.getName().length()-6);// 5. 动态加载该类Class?clazzClass.forName(className);// 6. 判断类上是否贴有自定义的 MyComponent 注解if(clazz.isAnnotationPresent(MyComponent.class)){MyComponentannotationclazz.getAnnotation(MyComponent.class);// 获取 Bean 的名字若注解中未填写则默认将类名首字母小写作为 Bean 名称StringbeanNameannotation.value();if(beanName.isEmpty()){beanNametoLowerCaseFirstOne(clazz.getSimpleName());}// 7. 通过反射动态创建对象并将其存入单例池 Map 中Objectinstanceclazz.getDeclaredConstructor().newInstance();singletonObjects.put(beanName,instance);System.out.println(【容器初始化】自动扫描并注册 Bean: beanName - clazz.getName());}}}}}}catch(Exceptione){e.printStackTrace();}}// 提供对外获取 Bean 的接口publicObjectgetBean(StringbeanName){returnsingletonObjects.get(beanName);}// 辅助工具方法将类名首字母转为小写privateStringtoLowerCaseFirstOne(Strings){if(Character.isLowerCase(s.charAt(0))){returns;}else{returnCharacter.toLowerCase(s.charAt(0))s.substring(1);}}}第四步测试运行在 Main.java 中编写我们的启动类packageorg.example;publicclassMain{publicstaticvoidmain(String[]args){System.out.println( 开始初始化 MySpring 容器 );// 1. 实例化我们的自定义容器指定要扫描的包路径MyApplicationContextcontextnewMyApplicationContext(org.example);System.out.println(容器初始化完成);// 2. 从容器中获取注册好的 Bean 并调用方法UserServiceuserService(UserService)context.getBean(userService);if(userService!null){userService.sayHello(张三);}OrderServiceorderService(OrderService)context.getBean(orderService);if(orderService!null){orderService.printOrder();}}}四、运行效果展示运行 Main 类的 main 方法后控制台输出如下 开始初始化 MySpring 容器 OrderService 对象已被创建 【容器初始化】自动扫描并注册 Bean: orderService - org.example.OrderService UserService 对象已被创建 【容器初始化】自动扫描并注册 Bean: userService - org.example.UserService 容器初始化完成 Hello, 张三这是来自 UserService 的问候。 订单模块您有一笔新的订单已生成。此时可以尝试在包下添加一个新的普通类不加 MyComponent 注解运行后会发现它并没有被自动实例化这也证实了注解在批量扫描中的过滤作用。结语写业务与写框架的思考写业务往往追求单点和直观用 new 就能快速解决问题。写框架追求规则的制定和通用性。框架作者使用一次性的复杂通用的反射、类加载与扫描机制来换取业务开发过程中的长期简单业务人员只需贴个注解即可。通过手写这几十行核心代码我们可以清晰地看到Spring 框架并没有深不可测的魔法其核心完全建立在 Java 基础的反射机制之上。