背景
最近在给公司写一些组件,自然会用到比较多的Spring拓展类,遇到一个奇异问题,结合Spring Bean创建过程,记录一下排查解决思路,现象:
- 使用到
ImportBeanDefinitionRegistrar
对组件包扫包(原因是组件项目与web服务包不一致,所以需要手动写BeanDefinitionScanner
进行扫包) - 使用到
ImportSelector
导入某配置类
最后发现两者一起使用后,会导致ImportSelector
导入的配置类中下面2个条件注解,对ImportBeanDefinitionRegistrar
自定义扫描器扫描的bean失效
@ConditionalOnBean
@ConditionalOnMissingBean
猜想:
@ConditionalOnMissingBean
为何失效,大概率是执行条件判断的时候Spring容器里面还没有初始化ImportBeanDefinitionRegistrar
所导出的Bean- 那猜想使用@Import导入的Bean的初始化优先级比较高
问题复现
先搭一个Demo复现问题
/**
* 一个老师的接口
* @author yejunxi 2022/06/29
*/
public interface Teacher {
}
/**
* 默认老师
*
* @author yejunxi 2022/06/29
*/
public class TeacherDefault implements Teacher {
}
/**
* 高级老师
*
* @author yejunxi 2022/06/29
*/
@Component
public class TeacherAdvanced implements Teacher {
}
先定义老师的类,我们想到达到的效果就是若Spring中没有高级老师任教,那就使用默认老师。要注意的是:
- 高级老师,使用了@Component,扫包自动注入Spring
- 默认老师,不使用@Component,由后面的配置TestAutoConfiguration导入,并作出条件限制
/**
* 配置类,用于判断是否存在高级老师,若无,则使用默认老师
*
* @author yejunxi 2022/06/29
*/
public class TestAutoConfiguration {
@Bean
@ConditionalOnMissingBean(Teacher.class)
public Teacher teacherDefault() {
return new TeacherDefault();
}
}
再来一个开启组件的注解,分别导入ImportSelector与ImportBeanDefinitionRegistrar
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({TestImportSelector.class, TestImportBeanDefinitionRegistrar.class})
public @interface TestEnable {
}
public class TestImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{TestAutoConfiguration.class.getName()};
}
}
public class TestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private Environment environment;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//扫描组件的包
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, true, environment);
scanner.scan("com.heys1.test");
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
组件的代码编写完毕,接下来使用Spring boot启动类测试,为模拟实际场景(组件包与启动类包不一样),我在test目录新增启动类
@SpringBootApplication
@TestEnable
public class TestApplication {
public static void main(String[] args) {
SpringApplication sa = new SpringApplication(TestApplication.class);
sa.run(args);
}
}
写一个测试类,引用老师
/**
* @author yejunxi 2022/06/29
*/
@Component
public class Tester {
@Autowired
Teacher teacher;
@PostConstruct
public void init(){
System.out.println(teacher);
}
}
最终的目录结构如图
运行,如无意外,将会报错
异常很简单,就是因为Spring容器中找到了2个Teacher,所以报错了
排查思路
为验证上面的猜想
我们先点进@ConditionalOnMissingBean,发现它判断逻辑的类是org.springframework.boot.autoconfigure.condition.OnBeanCondition
那我们先在org.springframework.boot.autoconfigure.condition.OnBeanCondition#getOutcomes
打断点,查看它的调用链路
了解过SpringBean声明周期都知道,Spring必然是在org.springframework.context.support.AbstractApplicationContext#refresh
对Bean进行初始化,我们先从此处入手
一条一条点一下,看是在哪儿创建配置类的BeanDefinition
然后发现有一个 processConfigBeanDefinitions 方法,看这方法名,似乎就是我想找的。再往上点org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
中的parser.parse(candidates)
就是问题所在
原因是经过多次断点观察,在本方法内的this.reader.loadBeanDefinitions(configClasses);
会对生成配置类的definition,而决定需要加载哪些配置类的代码是parser.parse(candidates)
,这个行代码是用来筛出哪些是ConfigurationClass,再对这些配置类进行初始化
我们取消运行刚刚的断点,重新在parser.parse(candidates)
打断点,看一下执行完这个方法后有什么变化
哦吼,在这个configurationClasses
里面居然找不到我们的高级老师类,我们放行掉这个断点,因为这是一个do循环,发现第二次循环就出现我们的高级老师类了
先说结论:
ImportBeanDefinitionRegistrar
所引入的类不算configurationClasses
,而且是比configurationClasses
更晚加载ImportBeanDefinitionRegistrar
和ImportSelector
本身均不会进入单例池
接下来我们只需搞清楚parser.parse(candidates);
搞了啥就行了
public void parse(Set<BeanDefinitionHolder> configCandidates) {
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
try {
if (bd instanceof AnnotatedBeanDefinition) {
//p1 我们的配置类都是通过注解注入的,所以会走此方法
parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
}
else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
}
else {
parse(bd.getBeanClassName(), holder.getBeanName());
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}
}
this.deferredImportSelectorHandler.process();
}
继续进去p1的方法,doProcessConfigurationClass应该就是核心逻辑
protected final SourceClass doProcessConfigurationClass(
ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
throws IOException {
略...
// Process any @ComponentScan annotations
// 在Spring Boot下,第一个加载的配置类是启动类,此处会找到 @ComponentScan,并注入包里面的Bean
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
// 被扫描的Bean
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// Process any @Import annotations
// 处理@Import的配置,我们启动类是通过
// @Import({TestImportSelector.class, TestImportBeanDefinitionRegistrar.class})
// 导入的这两个配置类,所以进去看看
processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
略...
}
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
//会走这里
this.importStack.push(configClass);
try {
for (SourceClass candidate : importCandidates) {
// 处理 ImportSelector
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
this.environment, this.resourceLoader, this.registry);
Predicate<String> selectorFilter = selector.getExclusionFilter();
if (selectorFilter != null) {
exclusionFilter = exclusionFilter.or(selectorFilter);
}
if (selector instanceof DeferredImportSelector) {
this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}
else {
// 递归操作,导入Bean,走到外层if判断的else分支上
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
}
}
//p2
//处理ImportBeanDefinitionRegistrar
//可以看出,此处并没有立即将里面的自定义BeanDefinitionScanner 所扫描的Bean作为配置类
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
根据p2处得知,处理ImportBeanDefinitionRegistrar时
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
只是使用一个Map将其保存起来,并没有执行
ImportBeanDefinitionRegistrar#registerBeanDefinitions()
,自然也不会扫到我们自定义包下的Bean
那我们看看这个Map在何处用到
IDEA 查看哪里调用到getImportBeanDefinitionRegistrars(),发现是在org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
那又是在哪儿调用这个方法呢,一直找上去,发现是
这行代码上面提到过,是用于实例化BeanDefinition,那到这流程就清晰了
- 第一步 启动类(xxApplication) 作为入口配置;
(parser.parse(candidates))
- 第二步 找到启动类中@ComponentScan 扫描的bean;
(parser.parse(candidates))
- 第三步 找到启动类中@Import 导入的bean;
(parser.parse(candidates))
- 按顺序加载上面配置类
第一次循环的this.reader.loadBeanDefinitions(configClasses);
- 加载完后再处理
ImportBeanDefinitionRegistrar
,若里面有导入的Bean则在第二次循环的this.reader.loadBeanDefinitions(configClasses);
才加载
核心方法均在org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
方法中下面的两行代码,多观察断点即可
parser.parse(candidates);
this.reader.loadBeanDefinitions(configClasses);