前面两篇文章(如何实现一个简易版的 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 配置文件:
<?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 "> <context:scann-package base-package="cn.mghio.service.version4,cn.mghio.dao.version4" /> </beans>我们期望的结果是解析出来的扫描包路径为: cn.mghio.service.version4、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 = return (StringUtils.hasLength(namespaceUri) && CONTEXT_NAMESPACE_URI.equals(namespaceUri)); } private boolean isDefaultNamespace(String namespaceUri) { // BEAN_NAMESPACE_URI = return (StringUtils.hasLength(namespaceUri) && BEAN_NAMESPACE_URI.equals(namespaceUri)); }第一个步骤就已经完成了,其实相对来说还是比较简单的,接下来看看第二步要如何实现。
对解析后的包路径进行扫描然后读取标有 @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 数组,实现如下:
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); } } } ... }上面的第一小步至此已经完成了,下面继续看第二小步。
② 读取转换好的 Resource 中的 @Component 注解