当前位置: 首页 > news >正文

手写一个mini版Spring:自动注册 + 依赖注入

写本系列文章的初衷:

在 AI 的时代浪潮下我们要时刻保持自己的思考,让 AI 为我们赋能,而不是替代我们。

所以我打算抽空学习关于 Spring 框架的一些知识,就是为了保持专注和探索, AI 的出现和发展会让任何行业的从业者都有可能成为程序员,到那个时候我们就仅仅是个程序员了。

本篇文章是作者的笔记,仅供参考。

上一篇文章我们实现了《手写一个mini版Spring》的第一个小节——手动注册 BeanDefinition 和获取单例Bean
手动注册 BeanDefinition 和获取单例Bean

在实际的项目开发中很少有人会使用手动来注册Bean,一般都是通过注解来实现,所以我们这期的内容就是:

  1. @Component 注解的实现
  2. @Autowired 注解的实现
  3. 字段注入的实现
  4. 包扫描的实现

也是就让容器从:手动注册 BeanDefinition 变成:自动扫描类 -> 注册 BeanDefinition -> 创建 Bean 时自动注入依赖

我们最终的效果就是:

@ComponentpublicclassOrderRepository{publicStringfindName(){return"我是 OrderRepository";}}@ComponentpublicclassOrderService{@AutowiredprivateOrderRepositoryorderRepository;publicStringfindName(){returnorderRepository.findName();}publicOrderRepositorygetOrderRepository(){returnorderRepository;}}@ComponentScan("com.example.step2")publicclassTestApplication{}classSimpleApplicationContextTest{@TestvoidscansComponentsAndAutowiresDependencies(){SimpleApplicationContextcontext=newSimpleApplicationContext(TestApplication.class);OrderServiceorderService=context.getBean(OrderService.class);System.out.println("获取到的 name : "+orderService.findName());assertEquals("我是 OrderRepository !",orderService.findName());}}

我把重点分了两个大类:

  1. 扫描:找到 @Component 类,注册为 BeanDefinition
  2. 注入:创建 Bean 的时候,扫描字段上的 @Autowired 注解,把依赖对象塞进去
1. 定义注解

@Component 注解:标记这个类要交给容器管理,标记此注解后会自动注册 Bean 定义。

// 作用:标记这个类要交给容器管理@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceComponent{/** * 组件名称 如果不写值,就默认类名首字母小写 */Stringvalue()default"";}

@Autowired 注解:标记此注解的字段支持自动注入,注入到引用这个字段的类的属性中

// 当前这一版暂时只支持字段注入、按类型注入@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceAutowired{}

@ComponentScan:标记配置类要扫描哪些包

// 标记配置类要扫码哪些包@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceComponentScan{/** * 组件名称 如果提供默认为bean的名称 */Stringvalue()default"";}
2. 改造 SimpleBeanFactory 类

在第一小节的时候已经写了一个 SimpleBeanFactory 类,主要用于组装bean的定义、注册bean的定义、管理bean的缓存等,所以接下来我们要来改造一下这个类。

手动注册 BeanDefinition 和获取单例Bean

2.1 实现通过按类型获取Bean接口

这一步其实是为了 @Autowired 提供服务,@Autowired 的本质就是通过字段类型去容器中查找 Bean。

BeanFactory 定义接口

// ...// 目前只贴了这个新加的接口<T>TgetBean(Class<T>requiredType);

SimpleBeanFactory 实现接口

// ...// 目前只贴了新增加的代码// 遍历Bean定义的Map,找到相同类型的Bean并返回// 目前这一版先:一个类型对应一个实现类,后面在处理负责的,如多实现类的情况@Overridepublic<T>TgetBean(Class<T>requiredType){for(StringbeanName:beanDefinitionMap.keySet()){BeanDefinitionbeanDefinition=beanDefinitionMap.get(beanName);Class<?>beanClass=beanDefinition.getBeanClass();if(requiredType.isAssignableFrom(beanClass)){returngetBean(beanName,requiredType);}}thrownewRuntimeException("No bean of type "+requiredType.getName()+" found.");}

2.2 优化 createBean() 方法

第一版 createBean() 方法是这样的

privateObjectcreateBean(StringbeanName,BeanDefinitionbeanDefinition){try{// 1. 创建bean实例Class<?>beanClass=beanDefinition.getBeanClass();Constructor<?>constructor=beanClass.getDeclaredConstructor();constructor.setAccessible(true);Objectbean=constructor.newInstance();// 2. 将bean实例注册到单例池中registerSingleton(beanName,bean);returnbean;}catch(Exceptione){thrownewRuntimeException(e);}}

我们接下来要做的就是:

在对象创建完之后,我们要扫描这个对象里的字段,把带有 @Autowired 的依赖注入进去,举个例子:

// 1. OrderService 中注入了 UserService 这个类型的字段// 2. 在创建 OrderService 实例的时候,就要通过 @Autowired 扫描到 UserService 然后把他也初始化到 OrderService 这个对象里@ComponentpublicclassOrderService{@AutowiredprivateUserServiceuserService;publicvoidtest(){System.out.println(userService);}}

开始改造 createBean() 方法:增加 populateBean(bean) 方法

privateObjectcreateBean(StringbeanName,BeanDefinitionbeanDefinition){try{// 1. 创建bean实例Class<?>beanClass=beanDefinition.getBeanClass();Constructor<?>constructor=beanClass.getDeclaredConstructor();constructor.setAccessible(true);Objectbean=constructor.newInstance();// 2. 依赖注入populateBean(bean);// 3. 将bean实例注册到单例池中registerSingleton(beanName,bean);returnbean;}catch(Exceptione){thrownewRuntimeException(e);}}

populateBean(bean) 方法实现

/** * 依赖注入 * @param bean */privatevoidpopulateBean(Objectbean){// 1. 获取并遍历当前bean的所有字段Field[]fields=bean.getClass().getDeclaredFields();for(Fieldfield:fields){// 2. 如果字段上没有@Autowired注解,就跳过if(!field.isAnnotationPresent(Autowired.class)){continue;}// 3. 否则:获取这个字段的类型,通过类型获取这个字段的bean实例Class<?>fieldType=field.getType();Objectdependency=getBean(fieldType);try{// 4. 通过反射 将这个字段的bean实例注入到当前bean中field.setAccessible(true);// 绕过 Java 的访问控制检查,如果字段是私有的,是不能在类外访问和修改的field.set(bean,dependency);}catch(IllegalAccessExceptione){thrownewRuntimeException("Failed to autowire field: "+field.getName(),e);}}}
3. 实现自动扫包、自动注册 Bean

上一步我们实现了依赖注入,但是他的前提是要注册 Bean 的定义,第一小节我们实现了通过手动的方式来注册 Bean 的定义, 这一节就学习如何自动注册。

我们要做的就是:通过包名,扫描这个包下面所有的 class ,如果包含我们定义的 @Component 注解,就把这个类注册到 BeanDefinition。

3.1 创建一个扫描器来实现这个需求

重点看 registerIfComponent(String className) 方法:带有 @Component 注解的类会被注册为 BeanDefinition,在 createBean 的时候会用这个定义来实例化 Bean。

/** * classpath 下的 BeanDefinition 扫描器。 * * <p>负责从指定包开始递归查找 class 文件,把标记了 @Component * 的类转换成 {@link BeanDefinition} 并注册到容器中。</p> * * @author : jiagang * @date : Created in 2026/6/5 14:14 */publicclassClassPathBeanDefinitionScanner{// BeanDefinition 注册入口,扫描器只负责发现候选类,真正的保存工作交给 registry。privatefinalBeanDefinitionRegistryregistry;publicClassPathBeanDefinitionScanner(BeanDefinitionRegistryregistry){this.registry=registry;}/** * 扫描一个或多个基础包。 * * @param basePackages 需要扫描的包名,例如 com.mdx.demo.service */publicvoidscan(String...basePackages){for(StringbasePackage:basePackages){scanPackage(basePackage);}}/** * 将 Java 包名转换成 classpath 路径,并定位对应目录。 */privatevoidscanPackage(StringbasePackage){// com.mdx.demo -> com/mdx/demo,用于从 ClassLoader 中查找资源目录。Stringpath=basePackage.replace('.','/');ClassLoaderclassLoader=Thread.currentThread().getContextClassLoader();URLresource=classLoader.getResource(path);if(resource==null){return;}// 当前实现只处理文件系统中的 class 目录,适合本项目的本地编译输出场景。FilebaseDir=newFile(resource.getFile());scanDirectory(basePackage,baseDir);}/** * 递归扫描目录中的 class 文件。 * * @param basePackage 当前目录对应的 Java 包名 * @param directory 当前要扫描的目录 */privatevoidscanDirectory(StringbasePackage,Filedirectory){File[]files=directory.listFiles();if(files==null){return;}for(Filefile:files){if(file.isDirectory()){// 子目录对应子包,继续向下扫描。scanDirectory(basePackage+"."+file.getName(),file);continue;}// 只处理编译后的 class 文件,忽略源码、资源文件等其他内容。if(!file.getName().endsWith(".class")){continue;}// 根据当前包名和文件名还原全限定类名,再判断是否需要注册。StringclassName=basePackage+"."+file.getName().replace(".class","");registerIfComponent(className);}}/** * 加载类并判断是否带有 @Component,符合条件时注册为 BeanDefinition。 */privatevoidregisterIfComponent(StringclassName){try{Class<?>clazz=Class.forName(className);if(!clazz.isAnnotationPresent(Component.class)){return;}// 读取 @Component 的 value,作为显式指定的 beanName。Componentcomponent=clazz.getAnnotation(Component.class);StringbeanName=component.value();if(beanName==null||beanName.isBlank()){// 没有显式命名时,沿用 Spring 风格:类名首字母小写作为 beanName。beanName=lowerFirst(clazz.getSimpleName());}// 将类信息包装成 BeanDefinition,后续创建 Bean 实例时会使用它。registry.registerBeanDefinition(beanName,newBeanDefinition(clazz));}catch(ClassNotFoundExceptione){thrownewRuntimeException("Failed to load class: "+className,e);}}/** * 将类名首字母小写,用于生成默认 beanName。 */privateStringlowerFirst(Stringname){if(name==null||name.isEmpty()){returnname;}returnCharacter.toLowerCase(name.charAt(0))+name.substring(1);}}

3.2 获取 @ComponentScan 注解的包名

扫描器实现完了,现在需要添加一个入口跟扫描器关联起来,就是要获取到 @ComponentScan 注解定义的包名,然后传给扫描器处理。

/** * @author : jiagang * @date : Created in 2026/6/5 17:54 */publicclassSimpleApplicationContext{privatefinalSimpleBeanFactorybeanFactory=newSimpleBeanFactory();publicSimpleApplicationContext(Class<?>configClass){scan(configClass);}// 获取 @ComponentScan 注解上的包名privatevoidscan(Class<?>configClass){if(!configClass.isAnnotationPresent(ComponentScan.class)){thrownewRuntimeException("No @ComponentScan found on config class: "+configClass.getName());}ComponentScancomponentScan=configClass.getAnnotation(ComponentScan.class);String[]basePackages=newString[]{componentScan.value()};ClassPathBeanDefinitionScannerscanner=newClassPathBeanDefinitionScanner(beanFactory);scanner.scan(basePackages);}publicObjectgetBean(StringbeanName){returnbeanFactory.getBean(beanName);}public<T>TgetBean(StringbeanName,Class<T>requiredType){returnbeanFactory.getBean(beanName,requiredType);}public<T>TgetBean(Class<T>requiredType){returnbeanFactory.getBean(requiredType);}}

至此所有实现就完成了。

4. 测试

首先实现以下测试类

@ComponentpublicclassOrderRepository{publicStringfindName(){return"我是 OrderRepository";}}@ComponentpublicclassOrderService{@AutowiredprivateOrderRepositoryorderRepository;publicStringfindName(){returnorderRepository.findName();}publicOrderRepositorygetOrderRepository(){returnorderRepository;}}@ComponentScan("com.example.step2")publicclassTestApplication{}classSimpleApplicationContextTest{// 并没有通过 SimpleBeanFactory 去注册类@TestvoidscansComponentsAndAutowiresDependencies(){SimpleApplicationContextcontext=newSimpleApplicationContext(TestApplication.class);OrderServiceorderService=context.getBean(OrderService.class);System.out.println("获取到的 name : "+orderService.findName());assertEquals("我是 OrderRepository !",orderService.findName());}}

运行 scansComponentsAndAutowiresDependencies() 测试方法,成功获取到 OrderRepository 类中的 findName。

最近看到一个很扎心的现象:企业越来越关注开发效率,而 AI 正在成为新的生产力工具。同样的需求,会使用 AI 的工程师往往能够更快完成设计、编码和测试工作。与其担心被 AI 替代,不如尽早学会驾驭 AI。最近我不仅在学习 Java 底层,还在学习一些人工智能的知识,发现了一个不错的 AI 学习网站,内容通俗易懂,比较适合程序员快速上手,感兴趣的话也可以看看:人工智能学习网

One more thing

要么努力到出类拔萃,要么就懒得乐知天命。

最怕你见识打开了,可努力又跟不上,骨子里清高至极,性格上又软弱无比。

http://www.rkmt.cn/news/1518044.html

相关文章:

  • 2026年西藏林芝靠谱的汽车贴膜门店榜单,贴车衣、汽车改色膜人气门店挑选指南 - 汽车新知百晓生
  • 2026 老旧品牌钻戒回收行情深度解析:五大商家实时行情报价实测测评 - 讯息早知道
  • 揭秘Scroll Reverser:3大技术突破终结macOS滚动混乱
  • 广州首饰回收靠谱指南|避坑 + 报价 + 流程全解析 - 讯息早知道
  • 甲基硫菌灵农药残留检测卡快速检测果蔬中的甲基硫菌灵农药残留
  • 2026昆明装修公司白皮书:5大本土实力装企实力解析 - 装修新知
  • 别再为论文配图发愁了!手把手教你用Ovito渲染LAMMPS轨迹文件(附气泡成核、结冰等案例)
  • 10分钟解锁微信语音:silk-v3-decoder如何让特殊音频格式重获新生
  • 2026视频号视频怎么保存到相册?视频号视频保存到相册方法全攻略
  • 第93篇 | HarmonyOS 生命周期刷新:返回页面后数据为什么要重新读
  • 第1节:初识C语言
  • 个人档案是什么终于搞懂了,毕业再也不怕处理档案了! - 慧办好
  • 2023-2025年江苏省省级企业技术中心名单深度分析报告
  • 使用语义分割经典模型 HRNet 训练道路分割模型并测试使用——从高分辨率特征到工程落地实践
  • 北京大兴区黄金回收店评测:三条核心指标筛选,爱回收12家门店全地址 - 新闻快传
  • 北京朝阳区黄金回收店推荐:爱回收24家门店全地址,选店三条标准说清楚 - 新闻快传
  • 2026年国内GEO服务商怎么选?这份指南帮你避开80%的踩坑风险 - 速递信息
  • 2026年中国GEO服务商综合实力权威测评排行榜,全栈自研标杆的泓动数据领跑GEO优化行业3.0时代 - 互联网科技品牌测评
  • 3个关键步骤让Citra模拟器在PC上流畅运行3DS游戏
  • 告别抠图!用Mask R-CNN实战分割商品图,Python+PyTorch保姆级教程
  • Vue-Fabric-Editor深度解析:插件化架构如何重构Web图片编辑体验
  • 2026在线音频转文字怎么操作?免费工具+详细上手教程
  • 货损降至0%!无锡靠谱物流公司推荐案例解析 - 速递信息
  • CI/CD前世今生(持续集成、持续交付、持续部署、Jenkins、Github Actions)
  • 工商业储能系列: BMS分散式主动均衡详解
  • 大雾速通
  • Poppins字体完整指南:多语言排版终极解决方案
  • Windows安卓应用安装器:3分钟快速在电脑上运行安卓应用
  • 保姆级教程:用OVITO的W-S法和表达式筛选,搞定晶界/晶内缺陷的精准分类统计
  • 图片去水印工具推荐:2026免费图片去水印工具实测