likes
comments
collection
share

【重写SpringFramework】配置类3:BeanMethod(chapter 3-7)Spring提供了两种通过

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

1. 前言

我们在第一章讲过 FactoryBean,特点是通过工厂方法来创建对象。FactoryBean 是编程式的解决方案,按照 Spring 框架的风格,对于同样的功能还会提供更为便捷的声明式解决方案。BeanMethod 是指声明了 @Bean 注解的方法,返回一个新的对象,然后注册到 Spring 容器中。

2. BeanMethod 概述

2.1 @Bean 注解

@Bean 注解只定义了一部分属性,还有一些属性比如 primaryrole 等可以通过 @Primary@Role 等注解来提供。

  • value:单例的名称,可使用 name 属性代替

  • name:单例的名称,可使用 value 属性代替

  • initMethod:指定单例的初始化方法

  • destroyMethod:指定单例的销毁方法

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
    @AliasFor("name")
    String value() default "";
    @AliasFor("value")
    String name() default "";
    String initMethod() default "";
    String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}

2.2 BeanMethod

BeanMethod 是对声明了 @Bean 注解的方法的抽象,只包含两个字段。metadata 表示方法的元数据,configurationClass 表示方法所属的配置类。BeanMethod 最终要通过反射的方式调用,因此方法以及对应的声明类都是必要的。

public class BeanMethod {
    private final MethodMetadata metadata;                  //方法的元数据
    private final ConfigurationClass configurationClass;    //外层的配置类
}

一个配置类中所有 BeanMethod 缓存在 ConfigurationClassbeanMethods 字段中,创建的类型是 LinkedHashSet。这一点很重要,因为一个配置类可能存在多个重载的 BeanMethod,添加的顺序是决定哪个 BeanMethod 最终被选定的条件之一。

public class ConfigurationClass {
    private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
}

3. BeanMethod 处理流程

3.1 概述

BeanMethod 的处理流程主要分为两块,首先是解析阶段,由 ConfigurationClassParserdoProcessConfigurationClass 方法进行处理,解析配置类中所有声明了 @Bean 注解的方法,并保存到 ConfigurationClass 中。

其次是注册阶段,由 ConfigurationClassBeanDefinitionReaderloadBeanDefinitionsForBeanMethod 方法处理。已解析的 BeanMethod 方法并不一定都会注册,需要经过条件判定和覆盖机制的两次过滤,对于符合条件的 BeanMethod 创建 ConfigurationClassBeanDefinition 对象,并注册到 BeanFactory 中。

【重写SpringFramework】配置类3:BeanMethod(chapter 3-7)Spring提供了两种通过

3.2 解析 BeanMethod

回到 ConfigurationClassParser 类的 doProcessConfigurationClass 方法,首先由retrieveBeanMethodMetadata 方法寻找配置类中所有声明了 @Bean 注解的方法,然后将这些方法封装成 BeanMethod 对象,存储在 ConfigClass 中。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//解析配置类
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
    //1. 处理内部类(略)
    //2. 处理配置文件(略)
    //3. 组件扫描(略)
    //4. 处理导入(TODO)
    //5. 处理工厂方法@Bean
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass.getMetadata());
    for (MethodMetadata beanMethod : beanMethods) {
        //此处仅注册BeanMethod,加载由ConfigurationClassBeanDefinitionReader完成
        configClass.addBeanMethod(new BeanMethod(beanMethod, configClass));
    }
}

retrieveBeanMethodMetadata 方法,首先获取当前配置类所有声明了 @Bean 注解的方法,然后判断配置类的元数据是不是 StandardAnnotationMetadata 的子类,也就是说配置类如果是通过反射的方式加载的,那么代码中方法的顺序可能会被打乱。此时 beanMethods 表示可能打乱顺序的方法集合,而 asmMethods 是指通过 ASM 方式获取的方法集合,然后以 asmMethods 集合的顺序为准,对 beanMethods 进行重新排序。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassParser]
//检索BeanMethod的元数据
private Set<MethodMetadata> retrieveBeanMethodMetadata(AnnotationMetadata metadata) throws IOException {
    Set<MethodMetadata> beanMethods = metadata.getAnnotatedMethods(Bean.class.getName());

    // 如果BeanMethod的个数大于0,且配置类是以反射的形式加载的,需要对BeanMethod进行排序,确保按照声明的顺序来加载
    if (beanMethods.size() > 1 && metadata instanceof StandardAnnotationMetadata) {
        AnnotationMetadata asm = this.metadataReaderFactory.getMetadataReader(metadata.getClassName()).getAnnotationMetadata();
        Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
        Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());

        //以AMS的方法顺序为准,将符合条件的BeanMethod加入集合中
        for (MethodMetadata asmMethod : asmMethods) {
            for (MethodMetadata beanMethod : beanMethods) {
                if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
                    selectedMethods.add(beanMethod);
                    break;
                }
            }
        }
        beanMethods = selectedMethods;
    }
    return beanMethods;
}

3.3 注册 BeanDefinition

接下来是 ConfigurationClassBeanDefinitionReaderloadBeanDefinitionsForConfigurationClass 方法。当所有的配置类解析完毕后,BeanMethod 的信息被保存在 ConfigurationClassbeanMethods 字段中。遍历整个集合,依次处理每个 BeanMethod。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
//加载BeanDefinition
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass) {
    //1. 注册配置类(略)
    //2. 注册BeanMethod
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        loadBeanDefinitionsForBeanMethod(beanMethod);
    }

    //3. 注册Import组件(略)
}

loadBeanDefinitionsForBeanMethod 方法的逻辑比较复杂,可以分为五步:

  1. 判断是否处理 BeanMethod(条件判定相关,待实现)
  2. 检查是否已经存在 BeanMethod,并决定是否覆盖已存在的 BeanMethod
  3. 根据 BeanMethod 创建 BeanDefinition,必须指定 factoryMethodName 属性,表明这是一个工厂方法
  4. 解析 @RolePrimary 等相关注解,并转化成 BeanDefinition 相关属性
  5. BeanDefinition 注册到容器中
//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
//从BeanMethod中加载BeanDefinition
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
    ConfigurationClass configClass = beanMethod.getConfigurationClass();
    MethodMetadata metadata = beanMethod.getMetadata();
    String methodName = metadata.getMethodName();

    //1. 条件判定,是否加载单例(TODO,待实现)

    //如果@Bean的value或name属性为空,使用方法名作为beanName
    AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
    String beanName = bean.getString("name");
    if(StringUtils.isEmpty(beanName)){
        beanName = methodName;
    }

    //2. 判断是否要覆盖可能已存在的BeanMethod对应的BeanDefinition
    if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
        return;
    }

    //3. 为BeanMethod创建BeanDefinition,并指定factoryMethodName属性,表明这是一个工厂方法
    ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass.getMetadata(), metadata);
    beanDef.setFactoryBeanName(configClass.getBeanName());  //设置FactoryBean名称,即外层的配置类
    beanDef.setUniqueFactoryMethodName(methodName);

    //4. 处理与@Bean相关的注解,比如@Role、@Primary等
    AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);
    //5. 将BeanDefinition注册到容器中
    this.registry.registerBeanDefinition(beanName, beanDef);
}

3.4 与依赖注入的联系

BeanDefinition 被注册到容器中之后,后续的处理过程都是由 BeanFactory 完成的。关于工厂方法的实例化我们已经在第一章详细介绍过了,相关代码参见 ConstructorResolverinstantiateUsingFactoryMethod 方法。由此可见,BeanMethod 的处理是依赖注入的方式之一,即工厂方法注入。示例代码如下,在创建 Foo 对象的过程中,完成了对参数 Bar 依赖项的解析。

//示例代码
public class XxxConfig {
    
    @Bean
    public class Foo(Bar bar) {
        return new Foo(bar);
    }
}

4. 覆盖机制

4.1 概述

一个类中可能存在多个重载方法,不同的类也可能存在同名的方法,因此我们必须考虑该由哪个 BeanMethod 来创建单例。当然 Spring 容器中已存在的同名 BeanDefinition 也可以通过组件扫描等方式加载,这些都涉及到 BeanMethod 的覆盖机制。总而言之,覆盖机制是指 BeanMethod 是否要覆盖容器中已存在的同名 BeanDefinition

【重写SpringFramework】配置类3:BeanMethod(chapter 3-7)Spring提供了两种通过

4.2 代码实现

isOverriddenByExistingDefinition 方法的作用是判断 BeanMethod 是否允许注册到 Spring 容器中,潜含义就是能否覆盖可能存在的同名 BeanDefinition。BeanMethod 的覆盖机制一共分为四种情况,接下来逐一进行分析。

第一种情况,容器中不存在同名的 BeanDefinition,允许覆盖,实际上就是直接注册。这是最基本的情况,之后的流程需要对已存在的 BeanDefinition 与当前的 BeanMethod 进行比较,再决定是否要覆盖。

第二种情况,如果已存在的 BeanDefinition 的类型是 ConfigurationClassBeanDefinition,说明是通过配置类加载的 BeanMethod。ccbd.getMetadata().getClassName() 表示已存在的 BeanMethod 所在配置类的名称,beanMethod.getConfigurationClass().getMetadata().getClassName() 表示当前 BeanMethod 所在配置类的名称。因此这一步比较的是两个 BeanMethod 所在配置类的名称是否相同,类名相同返回 true,否则返回 false,理由如下:

  • 如果类名相同则不覆盖,以先注册的为准。这种情况可能是配置类的两个 BeanMethod,也可能是配置类和父类各一个 BeanMethod。(父类是配置类的一部分)

  • 如果类名不同可以覆盖,后来者居上。这种情况可能是独立的两个类配置类,也可能是一个配置类和内部类。(内部配置类是独立的,且先于外部类处理)

第三种情况,已存在的 BeanDefinition 的类型是 ScannedGenericBeanDefinition,说明是以组件扫描的方式加载的。但是 BeanMethod 的优先级更高,允许覆盖。

第四种情况,如果已存在的 BeanDefinition 的角色是基础设施组件,那么允许进行覆盖。换句话说,框架自带的组件往往是用来兜底的,用户可以通过 BeanMethod 的方式来替换。

//所属类[cn.stimd.spring.context.annotation.ConfigurationClassBeanDefinitionReader]
//判断当前BeanMethod是否应该覆盖已存在的同名BeanDefinition
protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) {
    //1. 容器中不存在,直接返回先注册。否则需要与当前BeanMethod进行比较
    if (!this.registry.containsBeanDefinition(beanName)) {
        return false;
    }

    /* 2. BeanDefinition通过配置类创建的情况
     * 1) 不管是本类还是父类,如果两个BeanMethod的beanName相同,不覆盖,以最先加载的为准
     * 2) 如果A配置类和B配置类的BeanMethod的beanName相同,且A类先解析,那么B类会覆盖A类BeanMethod创建的BeanDefinition
     */
    BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName);
    if (existingBeanDef instanceof ConfigurationClassBeanDefinition) {
        ConfigurationClassBeanDefinition ccbd = (ConfigurationClassBeanDefinition) existingBeanDef;
        return ccbd.getMetadata().getClassName().equals(
            beanMethod.getConfigurationClass().getMetadata().getClassName());
    }

    //3. 已存在的BeanDefinition是通过扫描的方式加载的,但BeanMethod的优先级更高,允许覆盖
    if (existingBeanDef instanceof ScannedGenericBeanDefinition) {
        return false;
    }

    //4. 根据BeanDefinition的role属性来判断,允许应用程序的Bean覆盖框架生成的Bean
    if (existingBeanDef.getRole() > BeanDefinition.ROLE_APPLICATION) {
        return false;
    }
    return true;
}

4.3 深入分析

BeanMethod 的覆盖机制分为四种情况,此外,我们还可以从三个维度进行分析。

  • 不存在同名的 BeanDefinition,允许覆盖。但并不一定保证会注册成功,因为可能会被新的 BeanMethod 覆盖掉。
  • 从优先级的角度来说,BeanMethod 的优先级更高。基础设施组件和通过扫描加载的组件都会被覆盖,前者是框架自带的,后者是用户定义的。
  • 对于两个同名的 BeanMethod 来说,判断依据是 BeanMethod 所在的配置类的类名是否相同。

前两个维度是显而易见的,我们尤其关心第三个维度的判断依据。其一,如果配置类名相同,不覆盖,以先注册的为准。这里有两种情况,第一种情况,父类不是单独的配置类,而是属于配置子类的一部分。如下代码所示,两个 BeanMethod 所属的配置类名是同一个,即 BConfig。由于子类先被解析,因此最终加载的是 BConfig 定义的 BeanMethod。

//示例代码
//case-1:子类重写父类方法
public abstract class AConfig{
    @Bean
    public Foo foo() {
        return new Foo();
    }
}

@Configuration
public class BConfig extends AConfig{
    @Bean
    public Foo foo(){
        return new Foo();
    }
}

第二种情况,同一个配置类中有重载的 BeanMethod,方法名相同,但参数不同。此时先定义的 BeanMethod 会被注册。由于 JDK 反射可能会打乱方法的顺序,因此解析 BeanMethod 方法的时候,使用的是 ASM 框架来处理,这一点尤其需要注意。这种处理方式实际上非常有用,比如父类提供一个默认的组件,子类可以进行扩展,提供功能更为强大的新组件。当然子类也可以完成其他工作,由父类提供的组件负责兜底。

//示例代码
//case-2:同一个类中的重载方法
@Configuration
public class AConfig {
    @Bean
    public Foo foo(){
        return new Foo();
    }

    @Bean
    public Foo foo(Bar bar) {
        return new Foo();
    }
}

其二,配置类名不同,当前 BeanMethod 可以覆盖。如下代码所示,BConfig 为内部配置类。从解析顺序上来说,内部类优先被解析,其次是外部类,因此最终加载的是外部类的 BeanMethod。当然,我们可以把 BConfig 定义为独立的配置类,此时哪个 BeanMethod 会被注册视配置类的实际解析顺序而定。

//示例代码,不同配置类中的同名BeanMethod
@Configuration
public class AConfig {

    @Bean
    public Foo foo(){
        return new Foo();
    }

    @Configuration
    public class BConfig {
        @Bean
        public Foo foo(){
            return new Foo();
        }
    }
}

5. 测试

5.1 BeanMethod 重载

SubConfig 作为配置类,声明了一个 BeanMethod。SupperConfig 是父类,也声明了同名的 BeanMethod。本测试的目标是重载 BeanMethod 的覆盖情况,测试的情况是父类与子类的关系。此外,还可以是同一个类的重载方法,原理都是一样的。

//测试类
@Configuration
public class SubConfig extends SupperConfig {
    @Bean
    public User user(){
        return new User("User实例来自子类");
    }
}

//测试类,配置类的父类
public class SupperConfig {
    @Bean
    public User user(){
        return new User("User实例来自父类");
    }
}

在配置类的解析过程中,先处理父类,注册了父类定义的 BeanMethod。接着处理子类,发现了同名的 BeanMethod,结果子类的 BeanMethod 覆盖了先前注册的父类的 BeanMethod。

//测试方法
@Test
public void testOverrideBeanMethod() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SubConfig.class);
    User user = context.getBean(User.class);
    System.out.println(user.getName());
}

测试结果也印证了这一点,User 对象的确是来自子类。

User实例来自子类

5.2 同名 BeanMethod

首先定义两个配置类 AConfigBConfig,且都声明了同名的 BeanMethod 方法。

//测试类,定义同名的BeanMethod
@Configuration
public class AConfig {
    @Bean
    public User user(){
        return new User("User实例来自AConfig");
    }
}

@Configuration
public class BConfig {
    @Bean
    public User user(){
        return new User("User实例来自BConfig");
    }
}

在测试方法中,我们先注册 AConfig,后注册 BConfig,顺序很重要,结果会导致 BConfig 的 BeanMethod 最终被注册到 Spring 容器中。

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

    User user = context.getBean(User.class);
    System.out.println(user.getName());
}

从测试结果来看,也是符合预期的。如果将配置类的注册顺序调换,得到的结果就不同了。

User实例来自BConfig

5.3 扫描组件与 BeanMethod

首先定义一个 ScanConfig 配置类,并声明一个 BeanMethod,此外该配置类还声明了 @ComponentScan 注解,可以扫描该类所在目录下的组件 ScanBean

//测试类,扫描该类所在目录的组件
@ComponentScan
@Configuration
public class ScanConfig {

    @Bean
    public ScanBean scanBean() {
        return new ScanBean("ScanBean实例来自BeanMethod");
    }
}

在配置类的解析过程中,组件扫描的处理是先于 BeanMethod 执行的,因此通过扫描方式加载的 ScanBean 先被注册到容器中。然后是 BeanMethod 的处理,由于先注册的 BeanDefinition 的实际类型是 ScannedGenericBeanDefinition,因此后处理的 BeanMethod 会覆盖已存在的 BeanDefiniton

//测试方法
@Test
public void testScanAndBeanMethod() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
    ScanBean scanBean = context.getBean(ScanBean.class);
    System.out.println(scanBean.getName());
}

从测试结果可以看到,组件扫描的优先级较低,最终被通过 BeanMethod 加载的 BeanDefinition 覆盖。

ScanBean实例来自BeanMethod

5.4 测试 role 属性

准备一个新的配置类,定义一个 BeanMethod,表示 User 实例来自配置类。

//测试类
@Configuration
public class RoleConfig {
    @Bean
    public User user() {
        return new User("User实例来自配置类");
    }
}

在测试方法中,首先注册配置类 RoleConfig,然后创建了一个 RootBeanDefinition 实例,手动注册到容器中。需要注意的是,role 属性指定为 ROLE_INFRASTRUCTURE,也就是框架内部的基础设施组件。在配置类解析的过程中,将 BeanMethod 缓存起来,然后尝试进行注册,此时容器中已经存在同名的 BeanDefinition。由于手动注册的 BeanDefinition 的角色不是 ROLE_APPLICATION,因此会被 BeanMethod 覆盖。

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

    //手动注册User
    RootBeanDefinition definition = new RootBeanDefinition(User.class);
    definition.getPropertyValues().addPropertyValue("name", "User实例来自手动注册");
    definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    context.registerBeanDefinition("user", definition);
    context.refresh();

    User user = context.getBean(User.class);
    System.out.println("测试role属性:" + user.getName());
}

从测试结果可以看到,最终创建的实例来自 BeanMethod,先前注册的 BeanDefinitoin 被覆盖了。

测试role属性:User实例来自配置类

6. 总结

组件扫描和 BeanMethod 是 Spring 提供的两大加载组件的机制,前者负责加载大多数用户定义的组件,后者通常用来引入第三方框架的组件。此外,Spring 容器提供了两种通过工厂方法注册单例的方式,先前介绍的 FactoryBean 是编程式的工厂方法,而 BeanMethod 则是声明式的工厂方法。

BeanMethod 的重点在于覆盖机制,本节详细论述了四种引发覆盖的情况,并针对每种情况进行了说明和测试。Spring Boot 和 Spring Cloud 利用 BeanMethod 的覆盖机制,实现了非常灵活的加载。

7. 项目信息

新增修改一览,新增(9),修改(4)。

context
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.context
   │        └─ annotation
   │           ├─ Bean.java (+)
   │           ├─ BeanMethod.java (+)
   │           ├─ ConfigurationClass.java (*)
   │           ├─ ConfigurationClassBeanDefinitionReader.java (*)
   │           └─ ConfigurationClassParser.java (*)
   └─ test
      └─ java
         └─ context
             └─ config
                ├─ beanmethod
                │  ├─ scan
                │  │  ├─ ScanBean.java (+)
                │  │  └─ ScanConfig.java (+)
                │  ├─ AConfig.java (+)
                │  ├─ BConfig.java (+)
                │  ├─ RoleConfig.java (+)
                │  ├─ SubConfig.java (+)
                │  └─ SupperConfig.java (+)
                └─ ConfigTest.java (*)

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

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


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

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

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