likes
comments
collection
share

【重写SpringFramework】组件加载与AnnotationConfigApplicationContext(chapter 3-4)

作者站长头像
站长
· 阅读数 53

1. 前言

上一节介绍了 Spring 是如何将资源读取到内存中的,这里的资源是指广泛存在于各种介质中的数据。对于字节码文件来说,只拿到输入流或字节数组还不够,还需要知道类的信息。一是 JVM 类加载机制,二是通过 ASM 框架,Spring 对这两种解析方式进行了统一的抽象,相关的 API 定义在 Spring 核心包中。

在加载 class 文件、解析类的信息这两步之后,最后一步注册 BeanDefinition 的操作是现成的,调用 BeanDefinitionRegistry 接口的 registerBeanDefinition 方法即可。现在的问题是,之前我们一直使用 RootBeanDefinition 来创建实例,但这只是方便测试的权宜之计,一个大型应用可能有成百上千个组件,我们不可能一个个地手动创建。鉴于此,Spring 提供了强大易用的组件,不仅可以批量地处理字节码文件,还可以将以上三个步骤封装起来,提供一站式的解决方案。

2. AnnotatedBeanDefinition

BeanDefinition 的继承体系中,AnnotatedBeanDefinition 接口的作用是以注解声明的方式获取 BeanDefinition。由于一部分实现类在 context 模块,当时没有展开来讲。AnnotatedBeanDefinition 接口定义了两个方法,简单介绍如下:

  • getMetadata 方法:获取类的元数据
  • getFactoryMethodMetadata 方法:获取工厂方法的元数据(部分子类不支持工厂方法,直接返回 null)
public interface AnnotatedBeanDefinition extends BeanDefinition {
    AnnotationMetadata getMetadata();
    MethodMetadata getFactoryMethodMetadata();
}

AnnotatedBeanDefinition 接口有三个实现类,它们的结构和功能基本类似,主要是从用途和解析方式两个方面来分析。其特点如下:

  • AnnotatedGenericBeanDefinition:加载指定的类或元数据,支持反射和 ASM 解析(存在于 beans 模块中)
  • ScannedGenericBeanDefinition:通过扫描的方式加载 BeanDefinition,仅支持 ASM 解析
  • ConfigurationClassBeanDefinition:仅限配置类内部使用,负责处理 BeanMethod,支持反射和 ASM 解析(本节不涉及)

【重写SpringFramework】组件加载与AnnotationConfigApplicationContext(chapter 3-4)

3. AnnotatedBeanDefinitionReader

3.1 概述

AnnotatedBeanDefinitionReader 组件的作用是加载指定的 Bean,从功能上看与手动创建 BeanDefinition 实例差不多,但实际上有两点区别:

  • 自动注册了常用的组件,比如处理依赖注入和生命周期的 BeanPostProcessor 组件
  • 支持对 @Role@Primary 等辅助性注解进行解析,并设置成 BeanDefinition 对应的 roleprimary 等属性

AnnotatedBeanDefinitionReader 持有一个 BeanDefinitionRegistry 实例,实际上是 Spring 容器,用于注册 BeanDefinition。在构造方法中,通过 AnnotationConfigUtils 工具类注册了常用的 BeanPostProcessor 组件。

public class AnnotatedBeanDefinitionReader {
    private final BeanDefinitionRegistry registry;

    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
        this.registry = registry;
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }
}

AnnotationConfigUtils 是一个工具类,提供了与注解配置相关的方法。registerAnnotationConfigProcessors 方法的作用是注册相关的 BeanPostProcessor,比如支持依赖注入以及生命周期相关的组件。

public class AnnotationConfigUtils {

    public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
        //注册支持@Autowired和@Value注解的组件
        if(!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)){
            RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
            registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME);
        }

        //注册支持@PostConstruct和@PreDestroy注解的组件
        if (!registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
            RootBeanDefinition def = new RootBeanDefinition(InitDestroyAnnotationBeanPostProcessor.class);
            registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME);
        }
    }
}

3.2 register方法

register 方法用于注册指定的类,参数是 Class 类型,说明是通过反射的方式来加载类。

public void register(Class<?>... annotatedClasses) {
    for (Class<?> annotatedClass : annotatedClasses) {
        registerBean(annotatedClass, null);
    }
}

registerBean 方法完成了实际的加载逻辑,整个过程分为三步,如下所示:

  1. 根据条件判断是否加载,有关条件判定的内容将在后续介绍,先略过。

  2. 调用工具类 AnnotationConfigUtilsprocessCommonDefinitionAnnotations 方法,根据 @Role@Primary 等注解来设置相关属性。

  3. 生成 Bean 的名称,将 BeanDefinition 注册到 Spring 容器中。

private void registerBean(Class<?> annotatedClass, String name) {
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
    //1. 根据条件判定Bean是否加载(TODO 先略过,详见条件判定一节)

    //2. 根据@Role、@Primary等注解的信息来设置BeanDefinition的相关属性
    AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);

    //3. 注册BeanDefinition
    String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd));
    registry.registerBeanDefinition(beanName, abd);
}

4. ClassPathBeanDefinitionScanner

4.1 概述

ClassPathBeanDefinitionScanner 的作用是扫描指定目录及其子目录下的文件,对符合条件的类进行过滤,并注册到 Spring 容器中。扫描组件是一个批量操作,Spring 提供了一组注解来标识哪些类应该被加载。@Component 是一个元注解,可以声明在子注解上,因此 @Component 代表的是一组注解。cn.stimd.spring.context.stereotype 目录中定义了一部分子注解,如下所示:

  • @Controller 表示该组件是一个处理网络请求的接口类

  • @Service 表示组件是一个业务类

  • @Repository 表示组件与持久化有关,比如数据库相关的增删改查等操作

这些注解的共同特点是携带「语义化」,所谓语义化是指组件在系统内充当了某一角色。与之对比,@Component 没有具体的语义,泛指所有可以被 Spring 容器管理的组件。理论上,我们可以使用 @Component 注解完成所有的工作,但对一部分有着特殊用途的组件进行分类是良好的编程风格。

4.2 基本结构

ClassPathBeanDefinitionScannerClassPathScanningCandidateComponentProvider 的子类,两者的分工不同,父类只负责扫描类文件并转换成 BeanDefinition 集合,子类则将所有流程整合在一起。父类 ClassPathScanningCandidateComponentProvider 的主要属性如下所示:

  • includeFilters:包含规则,允许哪些类被加载,比如声明了 @Component 等注解
  • excludeFilters:排除规则,不允许哪些类被加载,这个需要自定义的指定
  • resourcePatternResolver:用于路径解析和资源加载
  • metadataReaderFactory:创建基于 ASM 元数据解析的工厂

一个目录下可能有多个类,那么哪些类应该加载,哪些类不应该加载,需要一个判断的依据。Spring 提供了 TypeFilter 来对类型进行过滤,includeFilters 属性是允许加载的过滤器集合,excludeFilters 是禁止加载的过滤器集合。为了方便称呼,将它们称为「包含规则」和「排除规则」。registerDefaultFilters 是一个重要的方法,作用是向包含规则中添加了一个过滤器,允许加载声明了 @Component 注解的类。

public class ClassPathScanningCandidateComponentProvider implements ResourceLoaderAware {
    private final List<TypeFilter> includeFilters = new LinkedList<>();
    private final List<TypeFilter> excludeFilters = new LinkedList<>();
    private ResourcePatternResolver resourcePatternResolver;
    private MetadataReaderFactory metadataReaderFactory;
    private Environment environment;

    //注册默认的包含过滤器
    protected void registerDefaultFilters(){
        this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    }
}

子类 ClassPathBeanDefinitionScanner 持有一个 BeanDefinitionRegistry 实例,作用是将已加载 BeanDefinition 集合注册到 Spring 容器中。该类在构造方法中调用了父类的 registerDefaultFilters 方法,支持注册声明了 @Component 注解的组件。

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
 	private final BeanDefinitionRegistry registry;

    public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, Environment environment) {
        this.registry = registry;
        registerDefaultFilters();       //注册默认的TypeFilter,允许加载@Component声明的类
        setResourceLoader((registry instanceof ResourceLoader ? (ResourceLoader) registry : null));
        setEnvironment(environment);
    }
}

4.3 scan 方法

子类 ClassPathBeanDefinitionScannerscan 方法是入口方法,实际的处理逻辑是由 doScan 方法完成的,然后使用工具类 AnnotationConfigUtils 注册相关的后处理器。

public void scan(String... basePackages){
    doScan(basePackages);
    AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

doScan 方法具体的处理过程分为三步,其中,后两步与 AnnotatedBeanDefinitionReader 基本相同,我们重点关心第一步是如何实现的。具体步骤如下:

  1. 扫描指定目录下,加载文件并转换成 BeanDefinition
  2. 生成 Bean 的名称,并根据 @Role@Primary 等注解来设置相关属性
  3. BeanDefinition 注册到 Spring 容器中
protected Set<BeanDefinitionHolder> doScan(String[] basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = new HashSet<>();
    for(String basePackage : basePackages){

        //1. 扫描指定目录,加载class文件,具体逻辑由父类完成
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        for (BeanDefinition candidate : candidates) {

            //2. 解析@Lazy、@Primary、@DependsOn、@Role、@Description等注解,为BeanDefinition添加相应属性
            String beanName = this.beanNameGenerator.generateBeanName(candidate);       //生成Bean的名称
            if (candidate instanceof AnnotatedBeanDefinition) {
                AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
            }

            //3. 检查是否与已存在的Bean冲突,如果没有则将BeanDefinition添加到容器中
            if(!this.registry.containsBeanDefinition(beanName)){
                registry.registerBeanDefinition(beanName, candidate);
                BeanDefinitionHolder holder = new BeanDefinitionHolder(candidate, beanName);
                beanDefinitions.add(holder);
            }
        }
    }
    return beanDefinitions;
}

4.4 加载和解析组件

findCandidateComponents 方法是由父类 ClassPathScanningCandidateComponentProvider 实现的,可以分为三步:

  1. 加载指定目录的资源。packageSearchPath 表示需要扫描的路径,格式为 classpath*:/xx/**/*.class,表示任意类路径下的、指定包路径下的、任意层级目录下的、任意名称的 class 文件

  2. 使用 MetadataReaderFactory 以 ASM 的方式获取注解元数据。

  3. 过滤出符合条件的类。这里有两层检查,首先是特殊规则的检查,然后是通用的检查。当候选项通过所有的检查,就可以作为 BeanDefinition 稍后注册到 Spring 容器中。

//扫描指定目录,加载候选的组件
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    //1. 加载指定目录下的资源
    basePackage = basePackage.replace('.', '/');        //将包名的.分隔符换成/
    //格式为classpath*:/xx/**/*.class,表示任意类路径下的、指定包路径下的、任意层级目录下的、任意名称的class文件
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + basePackage + '/' + DEFAULT_RESOURCE_PATTERN;
    Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);

    for (Resource resource : resources) {
        //2. 将加载的字节流以ASM的方式转换成Class元数据
        MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
        //3. 过滤出符合条件的类,创建BeanDefinition
        //3.1 根据TypeFilter和Condition过滤元数据
        if(isCandidateComponent(metadataReader)){
            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
            sbd.setResource(resource);

            //3.2 检查是否为实现类或静态内部类
            if(isCandidateComponent(sbd)){
                candidates.add(sbd);
            }
        }
    }
    return candidates;
}

我们需要对加载的元数据进行过滤,首先排除掉符合 excludeFilters 的候选项,然后让符合 includeFilters 的候选项通过。由此可见,排除规则的优先级高于包含规则。包含规则有一条默认的检查,之前已经提到过,就是检查类是否声明了 @Component 注解。此外,还需要进行条件判定,这部分内容先略过。

第一个 isCandidateComponent 方法是自定义检查,通过定义包含和排除规则,再配合条件判定,非常灵活。第二个 isCandidateComponent 方法是通用的检查,必须确保通过扫描加载的是一个普通类或静态内部类。

//3.1 根据排除或包含规则、以及条件判定,过滤出符合条件的候选项
private boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    //排除需要过滤掉的项(exclude规则的优先级高于include规则)
    for(TypeFilter tf: this.excludeFilters){
        if(tf.match(metadataReader, this.metadataReaderFactory)){
            return false;
        }
    }

    //加载指定规则的项,比如使用@Component注解声明的类
    for (TypeFilter tf : this.includeFilters) {
        if (tf.match(metadataReader, this.metadataReaderFactory)) {
            return true;
            //进一步检查Condition(TODO 详见条件判定一节)
        }
    }
    return false;
}

//3.2 通用检查,确保Bean是普通类或静态内部类
//1) 必须是独立的,即普通类或静态内部类
//2) 必须是具体的,即非抽象类或接口
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    AnnotationMetadata metadata = beanDefinition.getMetadata();
    return metadata.isIndependent() && metadata.isConcrete();
}

最后,回顾一下组件扫描的主要流程,如图所示。

【重写SpringFramework】组件加载与AnnotationConfigApplicationContext(chapter 3-4)

5. ApplicationContext 实现

AnnotationConfigApplicationContext 是通过注解声明的方式进行配置,这是本章最重要的 ApplicationContext 实现类。该类继承自 GenericApplicationContext,最大的特点是持有 AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner 这两个组件,拥有了多种方式来加载 Bean 的能力。

public class AnnotationConfigApplicationContext extends GenericApplicationContext {
    private final AnnotatedBeanDefinitionReader reader;
    private final ClassPathBeanDefinitionScanner scanner;

    public AnnotationConfigApplicationContext(){
        this.scanner = new ClassPathBeanDefinitionScanner(this);
        this.reader = new AnnotatedBeanDefinitionReader(this);
    }
}

6. 测试

6.1 加载指定的类

首先是准备工作。UserService 类使用 @Service 注解声明,表示一个业务类。UserController 使用 @Controller 注解声明,表示一个接口类。UserController 还通过依赖注入的方式持有 UserService 的实例。

//测试类,模拟业务层
@Service
public class UserService {
    public User getUser(){
        return new User();
    }
}

//测试类,模拟控制层
@Controller
public class UserController {
    @Autowired
    private UserService userService;


    public User getUser(){
        return this.userService.getUser();
    }
}

测试方法比较简单,首先创建 AnnotationConfigApplicationContext 实例,然后调用 register 方法完成了 UserService 的注册,再也不需要直接创建 RootBeanDefinition 实例。

//测试方法
@Test
public void testAnnotatedReader() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(UserService.class);
    context.refresh();

    UserService userService = context.getBean("userService", UserService.class);
    System.out.println("annotatedReader测试: " + userService.getUser());
}

从测试结果可以看到,UserService 被注册到了 Spring 容器中。

annotatedReader测试: context.basic.User@59e84876

6.2 类路径扫描

创建 AnnotationConfigApplicationContext 实例,调用 scan 方法扫描指定的目录,加载所有声明了 @Component 注解(及其子注解)的类,包括 UserControllerUserService。由于 AnnotationConfigUtils 工具类自动注册了 AutowiredAnnotationBeanPostProcessor,在获取 Bean 的流程中完成了依赖注入的功能。

//测试方法
@Test
public void testClassPathScanner(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.scan("context.basic");
    context.refresh();

    UserController userController = (UserController) context.getBean("userController");
    System.out.println("ClasspathScan测试: " + userController.getUser());
}

从测试结果可以看到,Spring 容器自动加载了 UserControllerUserService 两个单例,并完成了依赖注入的操作。

ClasspathScan测试: context.basic.User@26be92ad

7. 总结

我们知道,加载 Bean 的流程分为三步,资源加载、资源解析和注册 BeanDefinition,上一节实现了前两步,而第三步是现成的,本节的目标是将这三步整合起来。Spring 将这三个步骤封装到 AnnotatedBeanDefinitionReaderClassPathBeanDefinitionScanner 中,AnnotationConfigApplicationContext 作为最重要的实现类之一,持有这两个组件,从而拥有了通过注解声明的方式来启动应用的能力。

此外,Spring 使用注解元数据来描述类的信息,RootBeanDefinition 并不能体现出注解元数据的特性,而是通过 AnnotatedBeanDefinition 接口的实现类完成的。具体来说,AnnotatedGenericBeanDefinition 类专门用于 AnnotatedBeanDefinitionReader 组件,ScannedGenericBeanDefinition 类专门用于 ClassPathBeanDefinitionScanner 组件。

8. 项目结构

新增修改一览,新增(15),修改(1)。

context
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.context
   │        ├─ annotation
   │        │  ├─ AnnotatedBeanDefinitionReader.java (+)
   │        │  ├─ AnnotationBeanNameGenerator.java (+)
   │        │  ├─ AnnotationConfigApplicationContext.java (+)
   │        │  ├─ AnnotationConfigUtils.java (+)
   │        │  ├─ ClassPathBeanDefinitionScanner.java (+)
   │        │  ├─ ClassPathScanningCandidateComponentProvider.java (+)
   │        │  ├─ Primary.java (+)
   │        │  ├─ Role.java (+)
   │        │  └─ ScannedGenericBeanDefinition.java (+)
   │        └─ stereotype
   │           ├─ Component.java (+)
   │           ├─ Controller.java (+)
   │           ├─ Repository.java (+)
   │           └─ Service.java (+)
   └─ test
      └─ java
         └─ context
            └─ basic
               ├─ BasicTest.java (*)
               ├─ UserController.java (+)
               └─ UserService.java (+)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。

转载自:https://juejin.cn/post/7402060970426368009
评论
请登录