【重写SpringFramework】配置类3:BeanMethod(chapter 3-7)Spring提供了两种通过
1. 前言
我们在第一章讲过 FactoryBean
,特点是通过工厂方法来创建对象。FactoryBean
是编程式的解决方案,按照 Spring 框架的风格,对于同样的功能还会提供更为便捷的声明式解决方案。BeanMethod 是指声明了 @Bean
注解的方法,返回一个新的对象,然后注册到 Spring 容器中。
2. BeanMethod 概述
2.1 @Bean 注解
@Bean
注解只定义了一部分属性,还有一些属性比如 primary
、role
等可以通过 @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
缓存在 ConfigurationClass
的 beanMethods
字段中,创建的类型是 LinkedHashSet
。这一点很重要,因为一个配置类可能存在多个重载的 BeanMethod
,添加的顺序是决定哪个 BeanMethod
最终被选定的条件之一。
public class ConfigurationClass {
private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
}
3. BeanMethod 处理流程
3.1 概述
BeanMethod 的处理流程主要分为两块,首先是解析阶段,由 ConfigurationClassParser
的 doProcessConfigurationClass
方法进行处理,解析配置类中所有声明了 @Bean
注解的方法,并保存到 ConfigurationClass
中。
其次是注册阶段,由 ConfigurationClassBeanDefinitionReader
的 loadBeanDefinitionsForBeanMethod
方法处理。已解析的 BeanMethod 方法并不一定都会注册,需要经过条件判定和覆盖机制的两次过滤,对于符合条件的 BeanMethod 创建 ConfigurationClassBeanDefinition
对象,并注册到 BeanFactory
中。
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
接下来是 ConfigurationClassBeanDefinitionReader
的 loadBeanDefinitionsForConfigurationClass
方法。当所有的配置类解析完毕后,BeanMethod 的信息被保存在 ConfigurationClass
的 beanMethods
字段中。遍历整个集合,依次处理每个 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
方法的逻辑比较复杂,可以分为五步:
- 判断是否处理 BeanMethod(条件判定相关,待实现)
- 检查是否已经存在 BeanMethod,并决定是否覆盖已存在的 BeanMethod
- 根据 BeanMethod 创建
BeanDefinition
,必须指定factoryMethodName
属性,表明这是一个工厂方法 - 解析
@Role
、Primary
等相关注解,并转化成BeanDefinition
相关属性 - 将
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
完成的。关于工厂方法的实例化我们已经在第一章详细介绍过了,相关代码参见 ConstructorResolver
的 instantiateUsingFactoryMethod
方法。由此可见,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
。
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
首先定义两个配置类 AConfig
和 BConfig
,且都声明了同名的 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