likes
comments
collection
share

通过一行代码深入解析SpringBoot组件扫描和自动配置

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

通过这篇文章,你可以学会:

  1. SpringBoot组件扫描和自动配置的全流程
  2. SpringBoot组件扫描的路径顺序是如何确定的
  3. 条件注解在上述流程中是怎么生效的
  4. 条件注解在使用时有什么坑

使用的spring-boot-starter-parent版本为:2.4.4。展示的代码做了简化,隐藏了业务相关信息,但不影响理解。

问题背景

最近在工作中遇到了一个问题,在SpringBoot项目启动的时候出现了报错,具体信息如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'initJackson', defined in class path resource [com/peng/project/JacksonConfig.class], could not be registered. A bean with that name has already been defined in class path resource [com/peng/project/thirdparty/JacksonConfig.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true


很明显,有两个被 @Configuration 注解标记的 JacksonConfig,它们都有一个同名的 initJackson 方法,所以在启动的时候报错了。奇怪的是,其中一个initJackson方法是用了 @ConditionalOnMissingBean 注解标记的,而另一个却没有这个注解。按理说,@ConditionalOnMissingBean 标记的方法应该会被SpringBoot忽略掉,为什么会不生效呢?

具体的包和代码结构如下:

通过一行代码深入解析SpringBoot组件扫描和自动配置

一共有两个模块,boot模块表示项目的启动模块,而thirdparty模块则表示引用的组件库,可能是自研的也可能是第三方提供的。在这两个模块中,有如下的类:

通过一行代码深入解析SpringBoot组件扫描和自动配置

下面分别对boot模块和thirdparty模块的类进行介绍。

对于boot模块,ProjectMinimalsApplication 类表示SpringBoot的启动类,JacksonConfig类表示在boot模块中Jackson配置类,其代码如下:


// com.peng.project.ProjectMinimalsApplication
@SpringBootApplication(scanBasePackageClasses = ProjectMinimalsApplication.class, scanBasePackages = "com.peng.project.thirdparty")
public class ProjectMinimalsApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(ProjectMinimalsApplication.class)
                .beanNameGenerator(FullyQualifiedAnnotationBeanNameGenerator.INSTANCE)
                .run(args);
    }

}


// com.peng.project.JacksonConfig
@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer initJackson() {
        return builder -> {
            
        };
    }
}

对于thirdparty模块,FrameworkAutoConfiguration 类是组件库的自动配置类,JacksonConfig类是组件库中Jackson配置类,其代码如下:


// com.peng.project.thirdparty.FrameworkAutoConfiguration
@Import({
        JacksonConfig.class,
})
public class FrameworkAutoConfiguration {

}


// com.peng.project.thirdparty.JacksonConfig
@Configuration
public class JacksonConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public Jackson2ObjectMapperBuilderCustomizer initJackson() {
        return builder -> {
            
        };
    }
} 


FrameworkAutoConfiguration 是自动配置类,需要在thirdparty模块中配置META-INF/spring.factories 文件:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.thirdparty.FrameworkAutoConfiguration

代码就这么多,却在启动项目的时候,出现了上述错误。而且,当在META-INF/spring.factories文件增加thirdparty中的 JacksonConfig 配置时,项目却能正常启动了:


// 没有配置JacksonConfig,项目启动出错
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.thirdparty.FrameworkAutoConfiguration


// 配置了JacksonConfig,项目正常启动
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.thirdparty.JacksonConfig,\  
com.peng.project.thirdparty.FrameworkAutoConfiguration

这非常奇怪。thirdparty模块中的JacksonConfig 类明明已经被 @Configuration 注解标记了,为什么启动的时候 @ConditionalOnMissingBean 没有生效,而把 JacksonConfig 类加到自动配置类中的时候,却能正常启动?为解答这个问题,要先了解SpringBoot中被 @Configuration 注解的类 是如何被SpringBoot识别到的,也就是组件扫描和自动配置流程。

理解整体流程

熟悉SpringBoot的对Bean的生命周期都会有所了解。我们写的被@Component标记的或其派生注解(比如@Service@Configuration等)标记的类,是怎么在SpringBoot运行时变为Bean对象的呢?如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

在进入Bean的生命周期之前,需要找到这些被注解的类,将这些类定义解析成 BeanDefinition ,再根据BeanDefinition 信息构造出Bean对象。而将配置文件或配置类变为BeanDefinition 的方式有两种:组件扫描@ComponentScan 和自动配置 @EnableAutoConfiguration

那这两种方式是如何把配置类解析成BeanDefinition的呢?这还需要经过一些步骤,如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

在上面的例子中,我们在boot模块的Application类上用了@SpringBootApplication注解,这个注解本身就包含了 @ComponentScan 注解的功能。在thirdparty模块上用了自动配置,将FrameworkAutoConfiguration类变成了自动配置类。这是我们在项目中的配置。接着,这些配置类就会经过上面的流程,先将配置类转换成一个ConfigurationClass的集合,再将这个集合中的ConfigurationClass解析成BeanDefinition,然后注册到ApplicationContext中。这整个过程,其实都在ConfigurationClassPostProcessor类中的void processConfigBeanDefinitions(BeanDefinitionRegistry registry)方法完成。其核心逻辑如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

在这个方法中,和这个流程相关的代码就只有紫色框中的三行。对应图中的第1、2步:

  1. 先通过ConfigurationClassParserparse方法对配置类进行解析,解析后的ConfigurationClass保存在parser中
  2. 再由reader根据ConfigurationClass集合,将集合中的ConfigurationClass解析成 BeanDefinition,在void loadBeanDefinitions(Set<ConfigurationClass> configurationModel)方法内部进行Bean的注册,最终完成 配置类 -> ConfigurationClass -> BeanDefinition 的转换。

将配置类转换成 ConfigurationClass 的过程,就是上述问题的核心流程。于是,再深入对ConfigurationClassParserparse方法内部执行流程进行了解:


public void parse(Set<BeanDefinitionHolder> configCandidates) {
	for (BeanDefinitionHolder holder : configCandidates) {
		BeanDefinition bd = holder.getBeanDefinition();
		try {
			if (bd instanceof AnnotatedBeanDefinition) {
				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();
}

核心流程也不复杂,主要分为两部分:

  • 对组件扫描到的配置类转换成ConfigurationClass

第一次调用这个parse方法的时候,入参configCandidates集合中就只有一个元素,就是 Application 这个启动类所代表的BeanDefinitionHolder。自然地,这个启动类的BeanDefinitionHolder 属于 AnnotatedBeanDefinition,因此,就会进入到第一个if判断中进行解析。

  • 对自动配置类转换成ConfigurationClass

这一步就是通过deferredImportSelectorHandler.process() 实现的。除了将自动配置类转换成ConfigurationClass之外,标记在自动配置类上的@Import、方法返回的@Bean对象等都会被处理成ConfigurationClass

总的来说,上述流程可以用下面的图来概括:

通过一行代码深入解析SpringBoot组件扫描和自动配置

到了这一程度还不能解决开篇提到的问题,还需要继续往下探究组件扫描和自动配置的内部流程。

理解组件扫描的全流程

上面提到在ConfigurationClassParser类中,负责执行组件扫描的方法是一个parse方法,这里面还会去调用这些方法:


protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {  
	processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);  
}


protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
	if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
		return;
	}

	ConfigurationClass existingClass = this.configurationClasses.get(configClass);
	if (existingClass != null) {
		if (configClass.isImported()) {
			if (existingClass.isImported()) {
				existingClass.mergeImportedBy(configClass);
			}
			// Otherwise ignore new imported config class; existing non-imported class overrides it.
			return;
		}
		else {
			// Explicit bean definition found, probably replacing an import.
			// Let's remove the old one and go with the new one.
			this.configurationClasses.remove(configClass);
			this.knownSuperclasses.values().removeIf(configClass::equals);
		}
	}

	// Recursively process the configuration class and its superclass hierarchy.
	SourceClass sourceClass = asSourceClass(configClass, filter);
	do {
		sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
	}
	while (sourceClass != null);

	this.configurationClasses.put(configClass, configClass);
}

其中,核心逻辑是:


// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
	sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);

sourceClassconfigClass 就是我们在代码中写的组件,包括@Service@Controller等注解标记的类。configurationClasses就是保存着最终转换后的configurationClass的集合结果。而在doProcessConfigurationClass 方法中,又有一个较为复杂的流程,如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

图中表示的意思是,整个组件扫描的流程从processConfigurationClassdoProcessConfigurationClass开始,然后按照流程中的序号,依次进行处理。第1个处理的是被@Component注解标记的类及在它里面的配置类,第2个处理的是被@PropertySource注解标记的配置类,第3个处理的是被@ComponentScan注解标记的配置类...以此类推。一开始的入参是用于启动的Application类,因此要扫描到有冲突的JacksonConfig类的话,就要执行到第3步处理@ComponentScan注解才可以。对于@ComponentScan注解标记的类的处理流程如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

processComponentScan方法中,会引入ComponentScanAnnotationParser对象来进行扫描。我们写的配置类经过编译会变成class文件,通过scanCandidateComponents方法扫描之后,就会将class文件转换成Resource对象,然后经过excludeFiltersincludeFilters过滤之后,最终得到一个BeanDefinitionHolder的集合。

对于这个BeanDefinitionHolder的集合,得到的是一系列的配置类,配置类上会标记@Import@Bean等注解,所以又会调用parse方法进行解析,最终又回到doProcessConfigurationClass方法的处理流程中。整个过程其实就是一个递归扫描并将配置类转换成ConfigurationClass对象的过程。

理解自动配置的全流程

通过调试可以发现,在执行完parse方法之后,项目中的自动配置类并没有被转换成ConfigurationClass。SpringBoot将自动配置类转换成ConfigurationClass的处理,放在了deferredImportSelectorHandler.process()方法中。这涉及一个在处理自动配置类过程中比较重要的类:DeferredImportSelectorDefer是延迟的意思,DeferredImportSelector是一个延迟的import处理类,意思就是对于通过@Import的方式加载的配置类,会延迟加载,即待其他配置类加载完之后,再执行DeferredImportSelector进行加载。在process方法中涉及的处理类也比较复杂,如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

整个执行过程的时序图如图所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

从图可知,process()方法是一个入口方法,目的是为了将自动配置类及其通过@Import注入的配置类转换成ConfigurationClass。因此,通过调用processGroupImports()方法对DeferImportSelector按组进行批处理。在处理过程中又会调用到ConfigurationClassParser类中的processImports方法。而processImports方法的大致处理逻辑如下所示:

通过一行代码深入解析SpringBoot组件扫描和自动配置

因为@Import中通过ImportSelector注入的类是一个列表,而其他的普通配置类上也可以通过@Import注入,所以processImports是一个递归调用的过程,直到所有要注入的类都处理完毕了,整个processImports的过程才算结束。

整个过程可以参考下面这个简图:

通过一行代码深入解析SpringBoot组件扫描和自动配置

上面提到process方法的目的是将自动配置类及其通过@Import注入的配置类转换成ConfigurationClass,通过@Import注入的配置类是通过processImports方法的一系列递归调用转换成ConfigurationClass的,那自动配置类是什么时候转换的?其实,在processImports处理逻辑的思维导图里,对于其他类的处理,也包含了自动配置类。也就是说,当通过process方法第一次调用ConfigurationClassParserprocessImports方法的时候,传入的配置类就是自动配置类 FrameworkAutoConfiguration,执行的分支就是上面思维导图中的第三个分支:按普通配置类的处理分支。这样再结合上面提到的processConfiguration的核心流程:


// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
	sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);

this.configurationClasses.put(configClass, configClass);

doProcessConfigurationClass方法中解析自动配置类FrameworkAutoConfiguration上的所有模式注解,而在方法的最后,configurationClasses.put(configClass, configClass)则会将配置类填充到configurationClassess中,正是在这个时候将FrameworkAutoConfiguration对应的ConfigurationClass对象填充进去,正式转换成了ConfigurationClass对象。

至此,自动配置类转换成ConfigurationClass的整个流程就介绍完了。

问题出在了什么地方

理解了上面的整个流程,回归到问题本身,问题到底出现在流程的哪个环节中?结合上面的问题,其差异点在于有没有在spring.factories 文件中配置JacksonConfig类:


// 没有配置JacksonConfig,项目启动出错
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.thirdparty.FrameworkAutoConfiguration


// 配置了JacksonConfig,项目正常启动
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
com.peng.project.thirdparty.JacksonConfig,\  
com.peng.project.thirdparty.FrameworkAutoConfiguration

再结合上面对流程的分析,我们可以将问题再归纳成以下这个表:

情况使用何种方式转换成ConfigurationClass对象执行结果
JacksonConfig不是自动配置类组件扫描方式启动失败
JacksonConfig是自动配置类自动配置方式启动成功

为什么这两种情况会有不一样的结果呢?从整体流程上看,组件扫描方式会比自动配置方式先执行。也就是说,当使用组件扫描方式的时候,在组件库中的JacksonConfig类会和项目中的JacksonConfig类发生冲突,即组件库中的JacksonConfig类会先转换成ConfigurationClass,项目中的JacksonConfig类会后转换成ConfigurationClass,这样在解析这两个类中的@Bean方法时,就会先加载组件库中的JacksonConfig类,再加载项目中的JacksonConfig类,最终导致Bean加载出现了冲突。

可问题又来了,在组件库中JacksonConfig的@Bean方法已经加了 @ConditionalOnMissingBean注解,为什么不会把组件库中先加载的Bean忽略掉呢?我们可以从@ConditionalOnMissingBean的类注释上找到答案:

The condition can only match the bean definitions that have been processed by the application context so far and, as such, it is strongly recommended to use this condition on auto-configuration classes only. If a candidate bean may be created by another auto-configuration, make sure that the one using this condition runs after.

大致意思是,这个条件注解在执行时只会匹配已经被ApplicationContext处理过的BeanDefinition,因此强烈建议仅在自动配置类中使用条件注解。如果一个Bean被其他自动配置类创建时,也需要保证使用了条件注解的Bean要在没有使用条件注解的Bean之后进行注册。

也就是说,如果有两个相同的Bean A和B,A被标记了@ConditionalOnMissingBean,B没有被标记,那么当B先被ApplicationContext注册成BeanDefinition,A再注册,就会因为B已经存在于ApplicationContext中而不会再注册。反之,当A先进行注册,因为ApplicationContext中还没有与A相同的Bean,就会将A注册到ApplicationContext中。当B再执行注册时,因为B并没有被条件注解所标记,而又因为相同的Bean A已经在ApplicationContext中了,Spring此时并不会将已经注册了的A从ApplicationContext中移除,所以就只能抛出Bean冲突异常了。

相应的解决方案是,控制使用了条件注解的Bean的注册顺序。结合上面的流程可以知道,组件扫描会先于自动配置,所以当使用条件注解时,要让条件注册标记的Bean通过自动配置的方式进行注册。如果有多个自动配置类都注册了相同的Bean,则要通过AutoConfigureBeforeAutoConfigureAfterAutoConfigureOrder等注解控制自动配置的注册顺序。

使用组件扫描方式启动失败的原因

上面提到的条件注解的应用场景,正好解释了为什么通过组件扫描方式进行注册时,@ConditionalOnMissingBean没有生效。在上面的问题中,通过组件扫描的方式进行注册,其顺序是先扫描了组件库的JacksonConfig配置类,再扫描了项目的JacksonConfig配置类。然后,再按顺序加载Bean时,由于组件库的JacksonConfig配置排在前面,所以组件库中的Bean先被加载,在ApplicationContext中没有相同的Bean,所以@ConditionalOnMissingBean并没有起到作用,Bean会被加载到ApplicationContext中。然后项目中的JacksonConfig配置类再进行Bean加载,就会出现Bean的冲突的问题。

这还隐含了另一个问题:使用组件扫描的方式进行注册时,为什么它的执行顺序是先组件库的配置类,再扫描项目中的配置类?这取决于它在启动类Application中的配置:

// com.peng.project.ProjectMinimalsApplication
@SpringBootApplication(scanBasePackageClasses = ProjectMinimalsApplication.class, scanBasePackages = "com.peng.project.thirdparty")
public class ProjectMinimalsApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(ProjectMinimalsApplication.class)
                .beanNameGenerator(FullyQualifiedAnnotationBeanNameGenerator.INSTANCE)
                .run(args);
    }

}

在这个配置类中,配置了scanBasePackageClassesscanBasePackages两个属性。这两个属性都配置了的时候,到底是先扫描哪个呢?我们可以从源码中找到答案:


// org.springframework.context.annotation.ComponentScanAnnotationParser#parse

Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
	String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
			ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
	Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
	basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
	basePackages.add(ClassUtils.getPackageName(declaringClass));
}

scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
	@Override
	protected boolean matchClassName(String className) {
		return declaringClass.equals(className);
	}
});
return scanner.doScan(StringUtils.toStringArray(basePackages));

从代码中可以看到,扫描的顺序会放在basePackages的一个有序集合中。其顺序为:

  1. SpringBoot会先从配置中获取basePackages属性的值,然后加到basePackages结果集中。
  2. 然后再获取basePackageClasses属性的值对应的包路径,加到basePackages结果集中。
  3. 最后获取注解所标记的类的包路径,也就是启动类的所在的包路径,加到basePackages结果集中。这也就是为什么当我们没有配置basePackagesbasePackageClasses的时候,SpringBoot会默认扫描启动类所在的包路径的原因。

回到上面的问题,@SpringBootApplication(scanBasePackageClasses = ProjectMinimalsApplication.class, scanBasePackages = "com.peng.project.thirdparty")配置了两个属性,按照上面的源码规则,其扫描的顺序就是com.peng.project.thirdpartyProjectMinimalsApplication.class所在的包路径(即项目路径)。因此,通过组件扫描的方式加载Bean时,就会先扫描组件库中的配置类,再扫描项目中的配置类。

使用自动配置方式启动成功的原因

解释了为什么启动失败,接下来看下为什么使用自动配置的方式就启动成功。看到这个问题,可能你会觉得疑惑的是,JacksonConfig都通过自动配置的方式注册了,@ConditionalOnMissingBean自然就生效了,启动成功不就是一件很正常的事情吗?乍一看好像很正常,但仔细观察之后,这里面也挺不正常的。从上面的代码中可以看到,使用自动配置方式启动时,在thirdparty模块中的JacksonConfig类,也加上了@Configuration的注解:


// com.peng.project.thirdparty.JacksonConfig
@Configuration
public class JacksonConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public Jackson2ObjectMapperBuilderCustomizer initJackson() {
        return builder -> {
            
        };
    }
} 

此时组件扫描的过程仍然是生效的,也就是说,此时先执行组件扫描,会扫描到被@Configuration注解标记的JacksonConfig配置类,按理说,它应该会被当成普通配置类来处理,即使JacksonConfig类也是个自动配置类。这样的话,最终的结果应该也是启动失败才对,为什么现在看到的现象却是启动成功呢?

其中的原因在于,对于自动配置类的处理方式,和普通配置类的处理方式还真的不一样。我们可以看看@SpringBootApplication的注解定义:


@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Inherited  
@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),  
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })  
public @interface SpringBootApplication {
	...
}

在这个注解的定义中,有一个AutoConfigurationExcludeFilter类,用于对组件扫描到的类进行过滤。其内部实现为:


public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoaderAware {

	private ClassLoader beanClassLoader;

	private volatile List<String> autoConfigurations;

	@Override
	public void setBeanClassLoader(ClassLoader beanClassLoader) {
		this.beanClassLoader = beanClassLoader;
	}

	@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
			throws IOException {
		return isConfiguration(metadataReader) && isAutoConfiguration(metadataReader);
	}

	private boolean isConfiguration(MetadataReader metadataReader) {
		return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
	}

	private boolean isAutoConfiguration(MetadataReader metadataReader) {
		return getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
	}

	protected List<String> getAutoConfigurations() {
		if (this.autoConfigurations == null) {
			this.autoConfigurations = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
					this.beanClassLoader);
		}
		return this.autoConfigurations;
	}

}


match方法中,只有当这个类即使普通配置类,也是一个自动配置类的时候,会返回true,即同时满足这两种情况的类会被过滤掉。这样整个启动流程将会是:

  1. 应用启动,先扫描thirdparty模块(即组件库路径),再扫描boot模块(即项目路径)
  2. 当组件库中的JacksonConfig类加到自动配置类中,SpringBoot执行组件扫描时,发现JacksonConfig既是普通配置类(被@Configuuration注解标记),又是自动配置类,就会将其排除掉,这样组件扫描的结果中就不会包含thirdparty模块中的JacksonConfig类。
  3. 而在扫描boot模块时,因为boot模块的JacksonConfig类是一个普通配置类,不是自动配置类,所以会把boot模块中的JacksonConfig类扫描进去。
  4. 然后再执行自动配置类方式进行注册时,再把thirdparty模块中的JacksonConfig类注册进去。此时,注册顺序为:boot模块中的JacksonConfig类排在前面,thirdparty模块的JacksonConfig类排在后面。
  5. 加载Bean时,boot模块中的JacksonConfig类中的Bean先加载,thirdparty模块的JacksonConfig类的Bean后加载,由于上面介绍的@ConditionalOnMissingBean的特性,在ApplicationContext中已经有了相同的Bean,所以thirdparty模块的JacksonConfig类的Bean就不会再加载进去,也就不会产生Bean冲突。因此,项目在第2种情况下可以启动成功。

结合上述对ConfigurationClass的转换过程,可以得出这个处理是在processComponentScan()方法中进行的:

通过一行代码深入解析SpringBoot组件扫描和自动配置

至此,上述问题产生的原因已经非常清楚了。可见仅仅是一行代码的差异,背后的原理却相去甚远。不得不感叹,SpringBoot还是承受了太多了。

总结

从这个分析的过程我们可以知道:

  1. 配置类要变成Spring中的Bean,要先转换成ConfigurationClass,再转换成BeanDefinition
  2. 将配置类转换成ConfigurationClass有两种:组件扫描方式和自动配置方式。先执行组件扫描方式,再执行自动配置类方式
  3. 因为配置类有父子关系,类之间又可以有引用关系,所以转换成ConfigurationClass的过程是一个递归调用的过程
  4. 条件注解的原理是,当发现ApplicationContext中有相同的Bean时则不加载,否则也会加载进去。因此在使用条件注解时要注意Bean的加载顺序,建议在自动配置类中使用条件注解。如果有多个自动配置类,则需要通过注解控制自动配置类的加载顺序。
  5. SpringBoot组件扫描对包的扫描顺序是 scanBasePackages配置的包路径 -> scanBasePackageClasses配置的类所在的包路径 -> 启动类自身所在的包路径
  6. 因为要保证加载顺序是先执行组件扫描,再执行自动配置类,所以在组件扫描的过程中会过滤掉被@Configuraion标记的自动配置类。

经过对Bean加载顺序的分析,如果再遇到有Bean加载冲突或者Bean加载顺序不明确的问题,对着上面的图,按图索骥,细细分析下来就能弄清楚了。