前面两篇文章(如何实现一个简易版的 Spring - 如何实现 Setter 注入、如何实现一个简易版的 Spring - 如何实现 Constructor 注入)介绍的都是基于 XML 配置文件方式的实现,从 JDK 5 版本开始 Java 引入了注解支持,带来了极大的便利,Sprinng 也从 2.5 版本开始支持注解方式,使用注解方式咱们只需加上相应的注解便可,再也不须要去编写繁琐的 XML 配置文件,深受广大 Java 编程人员的喜好。接下来一块儿看看如何实现 Spring 框架中最经常使用的两个注解(@Component、@Autowired),因为涉及到的内容比较多,会分为两篇文章进行介绍,本文先来介绍上半部分 — 如何实现 @Component 注解。java
本文实现的注解虽说不用再配置 XML 文件,可是有点须要明确的是指定扫描 Bean 的包还使用 XML 文件的方式配置的,只是指定 Bean 再也不使用配置文件的方式。有前面两篇文章的基础后实现 @Component 注解主要分红如下几个步骤:git
下面咱们一步步来实现这几个步骤,最后去实现 @Component 注解:github
假设有以下的 XML 配置文件:spring
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.e3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/beans/spring-context.xsd"> <context:scann-package base-package="cn.mghio.service.version4,cn.mghio.dao.version4" /> </beans>
咱们指望的结果是解析出来的扫描包路径为: cn.mghio.service.version四、cn.mghio.dao.version4 。若是有仔细有了前面的文章后,这个其实就比较简单了,只须要修改读取 XML 配置文件的类 XmlBeanDefinitionReader 中的 loadBeanDefinition(Resource resource) 方法,判断当前的 namespace 是否为 context 便可,修改该方法以下:编程
public void loadBeanDefinition(Resource resource) { try (InputStream is = resource.getInputStream()) { SAXReader saxReader = new SAXReader(); Document document = saxReader.read(is); Element root = document.getRootElement(); // <beans> Iterator<Element> iterator = root.elementIterator(); while (iterator.hasNext()) { Element element = iterator.next(); String namespaceUri = element.getNamespaceURI(); if (this.isDefaultNamespace(namespaceUri)) { // beans parseDefaultElement(element); } else if (this.isContextNamespace(namespaceUri)) { // context parseComponentElement(element); } } } catch (DocumentException | IOException e) { throw new BeanDefinitionException("IOException parsing XML document:" + resource, e); } } private void parseComponentElement(Element element) { // 1. 从 XML 配置文件中获取须要的扫描的包路径 String basePackages = element.attributeValue(BASE_PACKAGE_ATTRIBUTE); // TODO 2. 对包路径进行扫描而后读取标有 @Component 注解的类,建立出对应的 BeanDefinition ... } private boolean isContextNamespace(String namespaceUri) { // CONTEXT_NAMESPACE_URI = http://www.springframework.org/schema/context return (StringUtils.hasLength(namespaceUri) && CONTEXT_NAMESPACE_URI.equals(namespaceUri)); } private boolean isDefaultNamespace(String namespaceUri) { // BEAN_NAMESPACE_URI = http://www.springframework.org/schema/beans return (StringUtils.hasLength(namespaceUri) && BEAN_NAMESPACE_URI.equals(namespaceUri)); }
第一个步骤就已经完成了,其实相对来讲仍是比较简单的,接下来看看第二步要如何实现。api
第二步是整个实现步骤中最为复杂和比较麻烦的一步,当面对一个任务比较复杂并且比较大时,能够对其进行适当的拆分为几个小步骤分别去实现,这里能够其再次拆分为以下几个小步骤:数组
第一小步主要是实现从一个指定的包路径下获取该包路径下对应的字节码文件并将其转化为 Resource 对象,将该类命名为 PackageResourceLoader,其提供一个主要方法是 Resource[] getResources(String basePackage) 用来将一个给定的包路径下的字节码文件转换为 Resource 数组,实现以下:框架
public class PackageResourceLoader { ... public Resource[] getResources(String basePackage) { Assert.notNull(basePackage, "basePackage must not be null"); String location = ClassUtils.convertClassNameToResourcePath(basePackage); ClassLoader classLoader = getClassLoader(); URL url = classLoader.getResource(location); Assert.notNull(url, "URL must not be null"); File rootDir = new File(url.getFile()); Set<File> matchingFile = retrieveMatchingFiles(rootDir); Resource[] result = new Resource[matchingFile.size()]; int i = 0; for (File file : matchingFile) { result[i++] = new FileSystemResource(file); } return result; } private Set<File> retrieveMatchingFiles(File rootDir) { if (!rootDir.exists() || !rootDir.isDirectory() || !rootDir.canRead()) { return Collections.emptySet(); } Set<File> result = new LinkedHashSet<>(8); doRetrieveMatchingFiles(rootDir, result); return result; } private void doRetrieveMatchingFiles(File dir, Set<File> result) { File[] dirContents = dir.listFiles(); if (dirContents == null) { return; } for (File content : dirContents) { if (!content.isDirectory()) { result.add(content); continue; } if (content.canRead()) { doRetrieveMatchingFiles(content, result); } } } ... }
上面的第一小步至此已经完成了,下面继续看第二小步。ide
要实现第二小步(读取转换好的 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 抽象类,用来获取字节码文件中类相关属性的提取,其代码实现以下所示:
/** * @author mghio * @since 2021-02-14 */ public class ClassMetadataReadingVisitor extends ClassVisitor implements ClassMetadata { private String className; private Boolean isInterface; private Boolean isAbstract; ... public ClassMetadataReadingVisitor() { super(Opcodes.ASM7); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = ClassUtils.convertResourcePathToClassName(name); this.isInterface = ((access & Opcodes.ACC_INTERFACE) != 0); this.isAbstract = ((access & Opcodes.ACC_ABSTRACT) != 0); ... } @Override public String getClassName() { return this.className; } @Override public boolean isInterface() { return this.isInterface; } @Override public boolean isAbstract() { return this.isAbstract; } ... }
第二个类 AnnotationMetadataReadingVisitor 用来获取注解的类型,而后经过构造方法传给 AnnotataionAttributesVisitor,为获取注解属性作准备,代码实现以下:
/** * @author mghio * @since 2021-02-14 */ public class AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor implements AnnotationMetadata { private final Set<String> annotationSet = new LinkedHashSet<>(8); private final Map<String, AnnotationAttributes> attributesMap = new LinkedHashMap<>(8); @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { String className = Type.getType(descriptor).getClassName(); this.annotationSet.add(className); return new AnnotationAttributesReadingVisitor(className, this.attributesMap); } @Override public boolean hasSuperClass() { return StringUtils.hasText(getSuperClassName()); } @Override public Set<String> getAnnotationTypes() { return this.annotationSet; } @Override public boolean hasAnnotation(String annotationType) { return this.annotationSet.contains(annotationType); } @Override public AnnotationAttributes getAnnotationAttributes(String annotationType) { return this.attributesMap.get(annotationType); } }
第三个类 AnnotationAttributesReadingVisitor 根据类 AnnotationMetadataReadingVisitor 传入的注解类型和属性集合,获取并填充注解对应的属性,代码实现以下:
/** * @author mghio * @since 2021-02-14 */ public class AnnotationAttributesReadingVisitor extends AnnotationVisitor { private final String annotationType; private final Map<String, AnnotationAttributes> attributesMap; private AnnotationAttributes attributes = new AnnotationAttributes(); public AnnotationAttributesReadingVisitor(String annotationType, Map<String, AnnotationAttributes> attributesMap) { super(Opcodes.ASM7); this.annotationType = annotationType; this.attributesMap = attributesMap; } @Override public void visit(String attributeName, Object attributeValue) { this.attributes.put(attributeName, attributeValue); } @Override public void visitEnd() { this.attributesMap.put(this.annotationType, this.attributes); } }
该类作的使用比较简单,就是当每访问当前注解的一个属性时,将其保存下来,最后当访问完成时以 K-V (key 为注解类型全名称,value 为注解对应的属性集合)的形式存入到 Map 中,好比,当我访问以下的类时:
/** * @author mghio * @since 2021-02-14 */ @Component(value = "orderService") public class OrderService { ... }
此时 AnnotationAttributesReadingVisitor 类的 visit(String, Object) 方法的参数即为当前注解的属性和属性的取值以下:
至此咱们已经完成了第二步中的前半部分的扫描指定包路径下的类并读取注解,虽然功能已经实现了,可是对应使用者来讲仍是不够友好,还须要关心一大堆相关的 Visitor 类,这里能不能再作一些封装呢?此时相信爱思考的你脑海里应该已经浮现了一句计算机科学界的名言:
计算机科学的任何一个问题,均可以经过增长一个中间层来解决。
仔细观察能够发现,以上读取类和注解相关信息的本质是元数据的读取,上文提到的 Resource 其实也是一中元数据,提供信息读取来源,将该接口命名为 MetadataReader,以下所示:
/** * @author mghio * @since 2021-02-14 */ public interface MetadataReader { Resource getResource(); ClassMetadata getClassMetadata(); AnnotationMetadata getAnnotationMetadata(); }
还须要提供该接口的实现,咱们指望的最终结果是只要面向 MetadataReader 接口编程便可,只要传入 Resource 就能够获取 ClassMetadata 和 AnnotationMetadata 等信息,无需关心那些 visitor,将该实现类命名为 SimpleMetadataReader,其代码实现以下:
/** * @author mghio * @since 2021-02-14 */ public class SimpleMetadataReader implements MetadataReader { private final Resource resource; private final ClassMetadata classMetadata; private final AnnotationMetadata annotationMetadata; public SimpleMetadataReader(Resource resource) throws IOException { ClassReader classReader; try (InputStream is = new BufferedInputStream(resource.getInputStream())) { classReader = new ClassReader(is); } AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(); classReader.accept(visitor, ClassReader.SKIP_DEBUG); this.resource = resource; this.classMetadata = visitor; this.annotationMetadata = visitor; } @Override public Resource getResource() { return this.resource; } @Override public ClassMetadata getClassMetadata() { return this.classMetadata; } @Override public AnnotationMetadata getAnnotationMetadata() { return this.annotationMetadata; } }
在使用时只须要在构造 SimpleMetadataReader 传入对应的 Resource 便可,以下所示:
到这里第二小步从字节码中读取注解的步骤已经完成了。
为了使以前定义好的 BeanDefinition 结构保持纯粹不被破坏,这里咱们再增长一个针对注解的 AnnotatedBeanDefinition 接口继承自 BeanDefinition 接口,接口比较简单只有一个获取注解元数据的方法,定义以下所示:
/** * @author mghio * @since 2021-02-14 */ public interface AnnotatedBeanDefinition extends BeanDefinition { AnnotationMetadata getMetadata(); }
同时增长一个该接口的实现类,表示从扫描注解生成的 BeanDefinition,将其命名为 ScannedGenericBeanDefinition,代码实现以下:
/** * @author mghio * @since 2021-02-14 */ public class ScannedGenericBeanDefinition extends GenericBeanDefinition implements AnnotatedBeanDefinition { private AnnotationMetadata metadata; public ScannedGenericBeanDefinition(AnnotationMetadata metadata) { super(); this.metadata = metadata; setBeanClassName(this.metadata.getClassName()); } @Override public AnnotationMetadata getMetadata() { return this.metadata; } }
还有一个问题就是使用注解的方式时该如何生成 Bean 的名字,这里咱们采用和 Spring 同样的策略,当在注解指定 Bean 的名字时使用指定的值为 Bean 的名字,不然使用类名的首字母小写为生成 Bean 的名字, 很明显这只是其中的一种默认实现策略,所以须要提供一个生成 Baen 名称的接口供后续灵活替换生成策略,接口命名为 BeanNameGenerator ,接口只有一个生成 Bean 名称的方法,其定义以下:
/** * @author mghio * @since 2021-02-14 */ public interface BeanNameGenerator { String generateBeanName(BeanDefinition bd, BeanDefinitionRegistry registry); }
其默认的生成策略实现以下:
/** * @author mghio * @since 2021-02-14 */ public class AnnotationBeanNameGenerator implements BeanNameGenerator { @Override public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { if (definition instanceof AnnotatedBeanDefinition) { String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); if (StringUtils.hasText(beanName)) { return beanName; } } return buildDefaultBeanName(definition); } private String buildDefaultBeanName(BeanDefinition definition) { String shortClassName = ClassUtils.getShortName(definition.getBeanClassName()); return Introspector.decapitalize(shortClassName); } private String determineBeanNameFromAnnotation(AnnotatedBeanDefinition definition) { AnnotationMetadata metadata = definition.getMetadata(); Set<String> types = metadata.getAnnotationTypes(); String beanName = null; for (String type : types) { AnnotationAttributes attributes = metadata.getAnnotationAttributes(type); if (attributes.get("value") != null) { Object value = attributes.get("value"); if (value instanceof String) { String stringVal = (String) value; if (StringUtils.hasLength(stringVal)) { beanName = stringVal; } } } } return beanName; } }
最后咱们再定义一个扫描器类组合以上的功能提供一个将包路径下的类读取并转换为对应的 BeanDefinition 方法,将该类命名为 ClassPathBeanDefinitionScanner,其代码实现以下:
/** * @author mghio * @since 2021-02-14 */ public class ClassPathBeanDefinitionScanner { public static final String SEMICOLON_SEPARATOR = ","; private final BeanDefinitionRegistry registry; private final PackageResourceLoader resourceLoader = new PackageResourceLoader(); private final BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) { this.registry = registry; } public Set<BeanDefinition> doScanAndRegistry(String packageToScan) { String[] basePackages = StringUtils.tokenizeToStringArray(packageToScan, SEMICOLON_SEPARATOR); Set<BeanDefinition> beanDefinitions = new HashSet<>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { beanDefinitions.add(candidate); registry.registerBeanDefinition(candidate.getId(), candidate); } } return beanDefinitions; } private Set<BeanDefinition> findCandidateComponents(String basePackage) { Set<BeanDefinition> candidates = new HashSet<>(); try { Resource[] resources = this.resourceLoader.getResources(basePackage); for (Resource resource : resources) { MetadataReader metadataReader = new SimpleMetadataReader(resource); if (metadataReader.getAnnotationMetadata().hasAnnotation(Component.class.getName())) { ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader.getAnnotationMetadata()); String beanName = this.beanNameGenerator.generateBeanName(sbd, registry); sbd.setId(beanName); candidates.add(sbd); } } } catch (IOException ex) { throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex); } return candidates; } }
到这里就已经把读取到的 @Component 注解信息转换为 BeanDefinition 了。
这一步其实并不须要再修改建立 Bean 的代码了,建立的逻辑都是同样的,只须要将以前读取 XML 配置文件那里使用上文提到的扫描器 ClassPathBeanDefinitionScanner 扫描并注册到 BeanFactory 中便可,读取配置文件的 XmlBeanDefinitionReader 类的读取解析配置文件的方法修改以下:
public void loadBeanDefinition(Resource resource) { try (InputStream is = resource.getInputStream()) { SAXReader saxReader = new SAXReader(); Document document = saxReader.read(is); Element root = document.getRootElement(); // <beans> Iterator<Element> iterator = root.elementIterator(); while (iterator.hasNext()) { Element element = iterator.next(); String namespaceUri = element.getNamespaceURI(); if (this.isDefaultNamespace(namespaceUri)) { parseDefaultElement(element); } else if (this.isContextNamespace(namespaceUri)) { parseComponentElement(element); } } } catch (DocumentException | IOException e) { throw new BeanDefinitionException("IOException parsing XML document:" + resource, e); } } private void parseComponentElement(Element element) { String basePackages = element.attributeValue(BASE_PACKAGE_ATTRIBUTE); // 读取指定包路径下的类转换为 BeanDefinition 并注册到 BeanFactory 中 ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry); scanner.doScanAndRegistry(basePackages); }
到这里实现 @Component 注解的主要流程已经介绍完毕,完整代码已上传至仓库 GitHub 。
本文主要介绍了实现 @Component 注解的主要流程,以上只是实现的最简单的功能,可是基本原理都是相似的,有问题欢迎留言讨论。下篇预告:如何实现 @Autowried 注解。