前言
前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入、如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML
配置文件方式的实现,从 JDK 5
版本开始 Java
引入了注解支持,带来了极大的便利,Sprinng
也从 2.5
版本开始支持注解方式,使用注解方式我们只需加上相应的注解即可,不再需要去编写繁琐的 XML
配置文件,深受广大 Java
编程人员的喜爱。接下来一起看看如何实现 Spring
框架中最常用的两个注解(@Component
、@Autowired
),由于涉及到的内容比较多,会分为两篇文章进行介绍,本文先来介绍上半部分 — 如何实现 @Component
注解。
实现步骤拆分
本文实现的注解虽然说不用再配置 XML
文件,但是有点需要明确的是指定扫描 Bean
的包还使用 XML
文件的方式配置的,只是指定 Bean
不再使用配置文件的方式。有前面两篇文章的基础后实现 @Component
注解主要分成以下几个步骤:
- 读取
XML
配置文件,解析出需要扫描的包路径 - 对解析后的包路径进行扫描然后读取标有
@Component
注解的类,创建出对应的BeanDefinition
- 根据创建出来的
BeanDefinition
创建对应的Bean
实例
下面我们一步步来实现这几个步骤,最后去实现 @Component
注解:
读取 XML
配置文件,解析出需要扫描的包路径
假设有如下的 XML
配置文件:
1 |
|
我们期望的结果是解析出来的扫描包路径为: cn.mghio.service.version4
、cn.mghio.dao.version4
。如果有仔细有了前面的文章后,这个其实就比较简单了,只需要修改读取 XML
配置文件的类 XmlBeanDefinitionReader
中的 loadBeanDefinition(Resource resource)
方法,判断当前的 namespace
是否为 context
即可,修改该方法如下:
1 | public void loadBeanDefinition(Resource resource) { |
第一个步骤就已经完成了,其实相对来说还是比较简单的,接下来看看第二步要如何实现。
对解析后的包路径进行扫描然后读取标有 @Component
注解的类,创建出对应的 BeanDefinition
第二步是整个实现步骤中最为复杂和比较麻烦的一步,当面对一个任务比较复杂而且比较大时,可以对其进行适当的拆分为几个小步骤分别去实现,这里可以其再次拆分为如下几个小步骤:
- 扫描包路径下的字节码(
.class
)文件并转换为一个个Resource
对象(其对于Spring
框架来说是一种资源,在Spring
中资源统一抽象为Resource
,这里的字节码文件具体为FileSystemResource
) - 读取转换好的
Resource
中的@Component
注解 - 根据读取到的
@Component
注解信息创建出对应的BeanDefintion
① 扫描包路径下的字节码(.class
)文件并转换为一个个 Resource
对象(其对于 Spring
框架来说是一种资源,在 Spring
中资源统一抽象为 Resource
,这里的字节码文件具体为 FileSystemResource
)
第一小步主要是实现从一个指定的包路径下获取该包路径下对应的字节码文件并将其转化为 Resource
对象,将该类命名为 PackageResourceLoader
,其提供一个主要方法是 Resource[] getResources(String basePackage)
用来将一个给定的包路径下的字节码文件转换为 Resource
数组,实现如下:
1 | public class PackageResourceLoader { |
上面的第一小步至此已经完成了,下面继续看第二小步。
② 读取转换好的 Resource
中的 @Component
注解
要实现第二小步(读取转换好的 Resource
中的 @Component
注解),首先面临的第一个问题是:如何读取字节码?
,熟悉字节结构的朋友可以字节解析读取,但是难度相对比较大,而且也比较容易出错,这里读取字节码的操作我们使用著名的字节码操作框架 ASM 来完成底层的操作,官网对其的描述入下:
ASM is an all purpose Java bytecode manipulation and analysis framework.
其描述就是:ASM
是一个通用的 Java
字节码操作和分析框架。其实不管是在工作或者日常学习中,我们对于一些比较基础的库和框架,如果有成熟的开源框架使用其实没有从零开发(当然,本身就是想要研究其源码的除外),这样可以减少不必要的开发成本和精力。ASM
基于 Visitor
模式可以方便的读取和修改字节码,目前我们只需要使用其读取字节码的功能。
ASM
框架中分别提供了 ClassVisitor
和 AnnotationVisitor
两个抽象类来访问类和注解的字节码,我们可以使用这两个类来获取类和注解的相关信息。很明显我们需要继承这两个类然后覆盖其中的方法增加自己的逻辑去完成信息的获取,要如何去描述一个类呢?其实比较简单无非就是 类名
、是否是接口
、是否是抽象类
、父类的类名
、实现的接口列表
等这几项。
但是一个注解要如何去描述它呢?注解其实我们主要关注注解的类型和其所包含的属性,类型就是一个 包名 + 注解名
的字符串表达式,而属性本质上是一种 K-V
的映射,值类型可能为 数字
、布尔
、字符串
以及 数组
等,为了方便使用可以继承自 LinkedHashMap<String, Object>
封装一些方便的获取属性值的方法,读取注解部分的相关类图设计如下:
其中绿色背景的 ClassVisitor
和 AnnotationVisitor
是 ASM
框架提供的类,ClassMetadata
是类相关的元数据接口,AnnotationMetadata
是注解相关的元数据接口继承自 ClassMetadata
,AnnotationAttributes
是对注解属性的描述,继承自 LinkedHashMap
主要是封装了获取指定类型 value
的方法,还有三个自定义的 Visitor
类是本次实现的关键,第一个类 ClassMetadataReadingVisitor
实现了 ClassVisitor
抽象类,用来获取字节码文件中类相关属性的提取,其代码实现如下所示:
1 | /** |
第二个类 AnnotationMetadataReadingVisitor
用来获取注解的类型,然后通过构造方法传给 AnnotataionAttributesVisitor
,为获取注解属性做准备,代码实现如下:
1 | /** |
第三个类 AnnotationAttributesReadingVisitor
根据类 AnnotationMetadataReadingVisitor
传入的注解类型和属性集合,获取并填充注解对应的属性,代码实现如下:
1 | /** |
该类做的使用比较简单,就是当每访问当前注解的一个属性时,将其保存下来,最后当访问完成时以 K-V
(key
为注解类型全名称,value 为注解对应的属性集合)的形式存入到 Map
中,比如,当我访问如下的类时:
1 | /** |
此时 AnnotationAttributesReadingVisitor
类的 visit(String, Object)
方法的参数即为当前注解的属性和属性的取值如下:
至此我们已经完成了第二步中的前半部分的扫描指定包路径下的类并读取注解,虽然功能已经实现了,但是对应使用者来说还是不够友好,还需要关心一大堆相关的 Visitor
类,这里能不能再做一些封装呢?此时相信爱思考的你脑海里应该已经浮现了一句计算机科学界的名言:
计算机科学的任何一个问题,都可以通过增加一个中间层来解决。
仔细观察可以发现,以上读取类和注解相关信息的本质是元数据的读取,上文提到的 Resource
其实也是一中元数据,提供信息读取来源,将该接口命名为 MetadataReader
,如下所示:
1 | /** |
还需要提供该接口的实现,我们期望的最终结果是只要面向 MetadataReader
接口编程即可,只要传入 Resource
就可以获取 ClassMetadata
和 AnnotationMetadata
等信息,无需关心那些 visitor
,将该实现类命名为 SimpleMetadataReader
,其代码实现如下:
1 | /** |
在使用时只需要在构造 SimpleMetadataReader
传入对应的 Resource
即可,如下所示:
到这里第二小步从字节码中读取注解的步骤已经完成了。
③ 根据读取到的 @Component
注解信息创建出对应的 BeanDefintion
为了使之前定义好的 BeanDefinition
结构保持纯粹不被破坏,这里我们再增加一个针对注解的 AnnotatedBeanDefinition
接口继承自 BeanDefinition
接口,接口比较简单只有一个获取注解元数据的方法,定义如下所示:
1 | /** |
同时增加一个该接口的实现类,表示从扫描注解生成的 BeanDefinition
,将其命名为 ScannedGenericBeanDefinition
,代码实现如下:
1 | /** |
还有一个问题就是使用注解的方式时该如何生成 Bean
的名字,这里我们采用和 Spring
一样的策略,当在注解指定 Bean
的名字时使用指定的值为 Bean
的名字,否则使用类名的首字母小写为生成 Bean
的名字, 很明显这只是其中的一种默认实现策略,因此需要提供一个生成 Baen
名称的接口供后续灵活替换生成策略,接口命名为 BeanNameGenerator
,接口只有一个生成 Bean
名称的方法,其定义如下:
1 | /** |
其默认的生成策略实现如下:
1 | /** |
最后我们再定义一个扫描器类组合以上的功能提供一个将包路径下的类读取并转换为对应的 BeanDefinition
方法,将该类命名为 ClassPathBeanDefinitionScanner
,其代码实现如下:
1 | /** |
到这里就已经把读取到的 @Component
注解信息转换为 BeanDefinition
了。
根据创建出来的 BeanDefinition
创建对应的 Bean
实例
这一步其实并不需要再修改创建 Bean
的代码了,创建的逻辑都是一样的,只需要将之前读取 XML
配置文件那里使用上文提到的扫描器 ClassPathBeanDefinitionScanner
扫描并注册到 BeanFactory
中即可,读取配置文件的 XmlBeanDefinitionReader
类的读取解析配置文件的方法修改如下:
1 | public void loadBeanDefinition(Resource resource) { |
到这里实现 @Component
注解的主要流程已经介绍完毕,完整代码已上传至仓库 GitHub 。
总结
本文主要介绍了实现 @Component
注解的主要流程,以上只是实现的最简单的功能,但是基本原理都是类似的,有问题欢迎留言讨论。下篇预告:如何实现 @Autowried
注解。