如何實現一個簡易版的 Spring - 如何實現 @Component 註解

前言

前面兩篇文章(如何實現一個簡易版的 Spring - 如何實現 Setter 注入如何實現一個簡易版的 Spring - 如何實現 Constructor 注入)介紹的都是基於 XML 配置文件方式的實現,從 JDK 5 版本開始 Java 引入了註解支持,帶來了極大的便利,Sprinng 也從 2.5 版本開始支持註解方式,使用註解方式咱們只需加上相應的註解便可,再也不須要去編寫繁瑣的 XML 配置文件,深受廣大 Java 編程人員的喜好。接下來一塊兒看看如何實現 Spring 框架中最經常使用的兩個註解(@Component@Autowired),因爲涉及到的內容比較多,會分爲兩篇文章進行介紹,本文先來介紹上半部分 — 如何實現 @Component 註解java

實現步驟拆分

本文實現的註解雖說不用再配置 XML 文件,可是有點須要明確的是指定掃描 Bean 的包還使用 XML 文件的方式配置的,只是指定 Bean 再也不使用配置文件的方式。有前面兩篇文章的基礎後實現 @Component 註解主要分紅如下幾個步驟:git

  1. 讀取 XML 配置文件,解析出須要掃描的包路徑
  2. 對解析後的包路徑進行掃描而後讀取標有 @Component 註解的類,建立出對應的 BeanDefinition
  3. 根據建立出來的 BeanDefinition 建立對應的 Bean 實例

下面咱們一步步來實現這幾個步驟,最後去實現 @Component 註解:github

讀取 XML 配置文件,解析出須要掃描的包路徑

假設有以下的 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.version4cn.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

對解析後的包路徑進行掃描而後讀取標有 @Component 註解的類,建立出對應的 BeanDefinition

第二步是整個實現步驟中最爲複雜和比較麻煩的一步,當面對一個任務比較複雜並且比較大時,能夠對其進行適當的拆分爲幾個小步驟分別去實現,這裏能夠其再次拆分爲以下幾個小步驟:數組

  1. 掃描包路徑下的字節碼(.class )文件並轉換爲一個個 Resource 對象(其對於 Spring 框架來講是一種資源,在 Spring 中資源統一抽象爲 Resource ,這裏的字節碼文件具體爲 FileSystemResource
  2. 讀取轉換好的 Resource 中的 @Component 註解
  3. 根據讀取到的 @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);
            }
        }
    }
  
  ...

}

上面的第一小步至此已經完成了,下面繼續看第二小步。ide

② 讀取轉換好的 Resource 中的 @Component 註解

要實現第二小步(讀取轉換好的 Resource 中的 @Component 註解),首先面臨的第一個問題是:如何讀取字節碼?,熟悉字節結構的朋友能夠字節解析讀取,可是難度相對比較大,並且也比較容易出錯,這裏讀取字節碼的操做咱們使用著名的字節碼操做框架 ASM 來完成底層的操做,官網對其的描述入下:學習

ASM is an all purpose Java bytecode manipulation and analysis framework.

其描述就是:ASM 是一個通用的 Java 字節碼操做和分析框架。其實不論是在工做或者平常學習中,咱們對於一些比較基礎的庫和框架,若是有成熟的開源框架使用其實沒有從零開發(固然,自己就是想要研究其源碼的除外),這樣能夠減小沒必要要的開發成本和精力。ASM 基於 Visitor 模式能夠方便的讀取和修改字節碼,目前咱們只須要使用其讀取字節碼的功能。

asm-sequence-diagram.png

ASM 框架中分別提供了 ClassVisitorAnnotationVisitor 兩個抽象類來訪問類和註解的字節碼,咱們可使用這兩個類來獲取類和註解的相關信息。很明顯咱們須要繼承這兩個類而後覆蓋其中的方法增長本身的邏輯去完成信息的獲取,要如何去描述一個類呢?其實比較簡單無非就是 類名是不是接口是不是抽象類父類的類名實現的接口列表 等這幾項。

可是一個註解要如何去描述它呢?註解其實咱們主要關注註解的類型和其所包含的屬性,類型就是一個 包名 + 註解名 的字符串表達式,而屬性本質上是一種 K-V 的映射,值類型可能爲 數字布爾字符串 以及 數組 等,爲了方便使用能夠繼承自 LinkedHashMap<String, Object> 封裝一些方便的獲取屬性值的方法,讀取註解部分的相關類圖設計以下:

spring-annotation-reading.png

其中綠色背景的 ClassVisitorAnnotationVisitorASM 框架提供的類,ClassMetadata 是類相關的元數據接口,AnnotationMetadata 是註解相關的元數據接口繼承自 ClassMetadataAnnotationAttributes 是對註解屬性的描述,繼承自 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-Vkey 爲註解類型全名稱,value 爲註解對應的屬性集合)的形式存入到 Map 中,好比,當我訪問以下的類時:

/**
 * @author mghio
 * @since 2021-02-14
 */
@Component(value = "orderService")
public class OrderService {

    ...

}

此時 AnnotationAttributesReadingVisitor 類的 visit(String, Object) 方法的參數即爲當前註解的屬性和屬性的取值以下:

annotatoin-attributes-reading.png

至此咱們已經完成了第二步中的前半部分的掃描指定包路徑下的類並讀取註解,雖然功能已經實現了,可是對應使用者來講仍是不夠友好,還須要關心一大堆相關的 Visitor 類,這裏能不能再作一些封裝呢?此時相信愛思考的你腦海裏應該已經浮現了一句計算機科學界的名言:

計算機科學的任何一個問題,均可以經過增長一箇中間層來解決。

仔細觀察能夠發現,以上讀取類和註解相關信息的本質是元數據的讀取,上文提到的 Resource 其實也是一中元數據,提供信息讀取來源,將該接口命名爲 MetadataReader,以下所示:

/**
 * @author mghio
 * @since 2021-02-14
 */
public interface MetadataReader {

    Resource getResource();

    ClassMetadata getClassMetadata();

    AnnotationMetadata getAnnotationMetadata();
}

還須要提供該接口的實現,咱們指望的最終結果是隻要面向 MetadataReader 接口編程便可,只要傳入 Resource 就能夠獲取 ClassMetadataAnnotationMetadata 等信息,無需關心那些 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 便可,以下所示:

metadata-reader.png

到這裏第二小步從字節碼中讀取註解的步驟已經完成了。

③ 根據讀取到的 @Component 註解信息建立出對應的 BeanDefintion

爲了使以前定義好的 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 了。

根據建立出來的 BeanDefinition 建立對應的 Bean 實例

這一步其實並不須要再修改建立 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 註解

相關文章
相關標籤/搜索