前言
当我们在配置类中通过@Bean
方法注入对象时,如果设置相同的beanName
,会出现注入对象不一致的问题。
1 现象
前提:设置配置类Config
,实现接口MyInterface
(接口中必须有一个@Bean
方法),同时配置类中设置两个@Bean
方法,这两个@Bean
名字设置为相同如下图:
设置主类如下:
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
User bean = context.getBean("ceshi1");
System.out.println(bean.getId());
}
运行结果如下图:
可以看出多次运行的话,每次注入的对象是随机的。
2 源码探究
从源码看,对配置类解析@Bean
方法时会出现不一致的顺序,具体如下
- 我们进入到
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry
中对配置类的解析,会进入到ConfigurationClassParser.parse
解析。 - 进入
processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter)
这个方法,这个方法对配置类解析如下:
进入doProcessConfigurationClass
:
/**
* 处理@Bean方法
*/
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
对@Bean
方法的处理最终调用retrieveBeanMethodMetadata
private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) {
AnnotationMetadata original = sourceClass.getMetadata();
Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName());
if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
// Try reading the class file via ASM for deterministic declaration order...
// Unfortunately, the JVM's standard reflection returns methods in arbitrary
// order, even between different runs of the same application on the same JVM.
/**上述通过反射获取的方法是随机的不是按照声明顺序、
* 通过asm技术获取确定的声明顺序
* 由于asm读取的是class文件,无法获取类继承接口信息,所有当继承接口中由其它@Bean方法,这里将直接使用jvm反射
* 获取的方法信息,也就是可能是无序的,
*/
try {
AnnotationMetadata asm =
this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
if (asmMethods.size() >= beanMethods.size()) {
Set<MethodMetadata> selectedMethods = new LinkedHashSet<>(asmMethods.size());
for (MethodMetadata asmMethod : asmMethods) {
for (MethodMetadata beanMethod : beanMethods) {
if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) {
selectedMethods.add(beanMethod);
break;
}
}
}
if (selectedMethods.size() == beanMethods.size()) {
// All reflection-detected methods found in ASM method set -> proceed
beanMethods = selectedMethods;
}
}
} catch (IOException ex) {
logger.debug("Failed to read class file via ASM for determining @Bean method order", ex);
// No worries, let's continue with the reflection metadata we started with...
}
}
return beanMethods;
}
从核心逻辑可以看出
- 首先是通过
jvm
的核心反射获取@Bean
修饰的方法(getAnnotatedMethods
同时会获取接口中实现的@Bean
方法),由于上述获取的@Bean
方法是以随机顺序返回的,不是声明的顺序。 - 所有,又使用
asm
技术读取class
字节码获取了@Bean
方法,主要是为了确保返回与声明一致的@Bean
方法。
但是上述代码中asmMethods.size() >= beanMethods.size()
这行代码导致了我们上述描述的现象,因为根据我们定义的Config
类(类中定义了2个@Bean
方法,继承接口中一个默认的@Bean
方法)总共实现了3个@Bean
方法。所以在通过反射获取时会获取到3个,通过asm
读取字节码只能读取到2个。导致后边执行逻辑不会进入,直接返回了反射获取的@Bean
方法,从而导致后续处理@Bean
方法时会出现随机注入的问题。
所以该问题的根本在于jvm
反射获取方法不是返回声明时的确定顺序。
3 测试
3.1 实现接口情况
如上述现象描述,我们debug到retrieveBeanMethodMetadata
方法结果如下图
可以看出此时用asm
处理顺序确定是失效的,没有进入处理逻辑
3.2 去掉实现类或者删除接口中@Bean方法
结果图如下
此时由于反射和asm
获取的@Bean
方法个数一致,从而进入了处理逻辑,此时运行返回的结果是一致的,始终是声明的顺序。
总结
从上述现象分析,jvm
的反射获取方法不是确定顺序对一些特殊的情况可能出现问题,但是通过asm
可以确保这一问题的解决。但是对于涉及到接口的情况,asm
处理起来会有些局限。