Creating Your Own Auto-configuration :: Spring Boot 中有以下内容:

NOTE

Auto-configurations must be loaded only by being named in the imports file. Make sure that they are defined in a specific package space and that they are never the target of component scanning. Furthermore, auto-configuration classes should not enable component scanning to find additional components. Specific @Import annotations should be used instead.

自动配置只能通过在 imports 文件(META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)中指定其名称的方式进行加载。请确保它们被定义在特定的包空间内,并且绝不能成为组件扫描的目标。此外,自动配置类不应启用组件扫描以查找其他组件。应使用特定的 @Import 注解来替代。

如何才能使自动配置类被 ComponentScan 扫描到

默认使用 @SpringBootApplication 注解时,ComponentScan不会扫描自动配置类,因为 SpringBootApplication 注解上的 @ComponentScan 配置了一个 AutoConfigurationExcludeFilter 类型的 excludeFilters,它会在组件扫描时排除掉在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册的和有 @AutoConfiguration 注解的类。

@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 {}
public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoaderAware {

	@SuppressWarnings("NullAway.Init")
	private ClassLoader beanClassLoader;

	private volatile @Nullable List<String> autoConfigurations;

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

	@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
			throws IOException {
        // 如果有 @Configuration 注解且是自动配置类
		return isConfiguration(metadataReader) && isAutoConfiguration(metadataReader);
	}

	private boolean isConfiguration(MetadataReader metadataReader) {
        // 是否有 @Configuration
		return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
	}

	private boolean isAutoConfiguration(MetadataReader metadataReader) {
        // 有 @AutoConfiguration
		boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata()
			.isAnnotated(AutoConfiguration.class.getName());
		return annotatedWithAutoConfiguration
            // 存在于 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中
				|| getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
	}

	protected List<String> getAutoConfigurations() {
		List<String> autoConfigurations = this.autoConfigurations;
		if (autoConfigurations == null) {
            // 加载 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件
			ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.beanClassLoader);
			autoConfigurations = importCandidates.getCandidates();
			this.autoConfigurations = autoConfigurations;
		}
		return autoConfigurations;
	}
}
public final class ImportCandidates implements Iterable<String> {
	
	private static final String LOCATION = "META-INF/spring/%s.imports";
	
    // 传入的 annotation 参数是 AutoConfiguration.class
	public static ImportCandidates load(Class<?> annotation, @Nullable ClassLoader classLoader) {
		Assert.notNull(annotation, "'annotation' must not be null");
		ClassLoader classLoaderToUse = decideClassloader(classLoader);
        // annotation.getName() 为 org.springframework.boot.autoconfigure.AutoConfiguration
        // LOCATION = "META-INF/spring/%s.imports
        // 格式化结果就是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
		String location = String.format(LOCATION, annotation.getName());
        // 获取多个 imports 文件的路径
		Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
		List<String> importCandidates = new ArrayList<>();
        // 循环读取 imports 文件
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
            // readCandidateConfigurations 读取文件内容
			importCandidates.addAll(readCandidateConfigurations(url));
		}
		return new ImportCandidates(importCandidates);
	}
    
	private static List<String> readCandidateConfigurations(URL url) {
		try (BufferedReader reader = new BufferedReader(
				new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
			List<String> candidates = new ArrayList<>();
			String line;
            // 逐行读取
			while ((line = reader.readLine()) != null) {
				line = stripComment(line);
				line = line.trim();
				if (line.isEmpty()) {
					continue;
				}
                // 一行一个配置类
				candidates.add(line);
			}
			return candidates;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load configurations from location [" + url + "]", ex);
		}
	}
}

在自定义配置类中添加无 AutoConfigurationExcludeFilterComponentScan 即可扫描到自动配置类:

@Configuration
@ComponentScan(basePackages = {"org.springframework.boot.autoconfigure.**"})
public class TestConfiguration {
}

可能的原因

spring 只说了自动配置类绝不能成为组件扫描的目标,但没有说明原因,以下是我认为可能的原因。

违背了自动配置的初衷

Spring Boot 中存在两套独立的“发现与加载”机制,它们的执行时机和目的截然不同:

  • 组件扫描,属于用户配置阶段,由 @ComponentScan 控制:
    • 目的:用于发现用户或者说应用相关的配置、Bean,比如 @Controller, @Service, @Component 等用户自己写的代码。
    • 时机:较早期,且一般是无条件的。
    • 范围:会无脑扫描 basePackages 下的所有文件。
  • 自动配置,属于自动配置阶段,由 @EnableAutoConfiguration 控制:
    • 目的:用于发现基础设施相关的配置、Bean(框架、第三方提供的能力)。
    • 时机:在用户配置阶段之后,这时用户定义的 BeanDefinition 都已经加载完毕,可以作为自动配置的 @Conditional 的判断依据了。
    • 范围:只会扫描 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中定义的类。

@ConditionalOnMissingBean@ConditionalOnProperty 条件注解失效

@ConditionalOnMissingBean 当不存在某个 Bean 时才执行配置。

@ConditionalOnProperty 当属性满足某个条件时才执行配置,依赖于 Environment

正常流程

用户配置阶段加载用户 BeanDefinition;处理用户 @PropertySource,将其添加到 Environment 中。

自动配置阶段根据已经加载的用户 BeanDefinition 来判断是否满足 @ConditionalOnMissingBean;根据 Environment 判断是否满足 @ConditionalOnProperty

被扫描后

自动配置类和用户配置类混在一起,处理顺序无法保证,可能出现以下情况:

此时用户 Bean 还没创建 → @ConditionalOnMissingBean 判断为不存在 → 自动配置先创建 Bean

→ 后续用户配置加载时,发现 Bean 已存在 → 报错:Bean 重复定义。

此时用户 @PropertySource 还没处理 → @ConditionalOnProperty 判断条件不满足 → 自动配置不会执行 → 处理用户 @PropertySource 使 @ConditionalOnProperty 条件变为满足。这会导致本该执行的自动配置没被执行,反之亦然。

@EnableConfigurationProperties 绑定空值

@ConditionalOnProperty 类似,@EnableConfigurationProperties 也依赖于 Environment@EnableConfigurationProperties 先于配置文件处理会导致绑定空值。

自动配置的 exclude 失效

自动配置类可以被以下方式排除,但是组件扫描不会考虑这些排除配置,这会导致自动配置类的排除失效。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

@SpringBootApplication(excludeName = "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration")

@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)

@EnableAutoConfiguration(excludeName = "org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration")

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration

自动配置类的顺序失效

自动配置类可以通过 @AutoConfigureBefore@AutoConfigureAfter@AutoConfiguration(before, beforeName, after, afterName) 注解定义自动配置的执行顺序与依赖关系。组件扫描不会考虑这些排序注解,导致处理顺序混乱。

比如 MybatisAutoConfiguration 有注解 @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })。如果 MybatisAutoConfiguration 先于 DataSourceAutoConfiguration 被处理会导致 @ConditionalOnSingleCandidate(DataSource.class) 条件不满足,导致 Mybatis 没被自动配置,进而导致应用启动失败。

自动配置类被处理两遍

若自动配置类被组件扫描则会被处理两遍,第一遍是被组件扫描处理,第二遍是被自动配置处理。一方面浪费资源,另一方面可能导致未知问题。

产生不必要的性能开销

自动配置类通常位于第三方 Jar 包中(如 spring-boot-autoconfigure)。如果组件扫描不加过滤地去扫描所有 Jar 包,会极大地拖慢启动速度。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐