前言
前面兩篇文章(如何實現一個簡易版的 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
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.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 = 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));
}
第一個步驟就已經完成了,其實相對來說還是比較簡單的,接下來看看第二步要如何實現。
對解析后的包路徑進行掃描然后讀取標有 @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 注解
要實現第二小步(讀取轉換好的 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 即可,如下所示:
到這里第二小步從字節碼中讀取注解的步驟已經完成了。
③ 根據讀取到的 @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 注解。