SpringBoot - 自动装配深入解析与简单实现

212 阅读7分钟

前言

越向往光明,越要默默的扎根于黑暗

本篇文章已收录到 GitHub 仓库 github.com/logerJava/l…

在之前我们已经学习 Spring 中各式各样的知识点, 关于 Spring MVC 的话就不做多讲解了, 目前市场上除了一些老项目应该很少有地方会用 Spring MVC 框架了, 取而代之的是 SpringBoot, 就算是分布式项目 SpringCloud 中也是由一个个小的 SpringBoot 组成的, 本篇文章就来了解一下 loger 认为 SpringBoot 受欢迎的一个重要原因 - 自动装配

SpringBoot 自动装配原理

如果大家有自己搭建过 SpringBoot 项目, 一定会发现, 配置起来非常简单, 无需繁琐的配置文件也没有各种复杂的 pom 操作, 那么这么简便的原因是什么呢 ? 如果小伙伴们细心的话一定会发现, SpringBoot 的 pom 文件中有很多带有 'starter' 的包

starter包.png

这些就是关键, 我们来看一下要实现自动装配需要了解的三个关键注解

@SpringBootApplication
public class ApplicationRun {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationRun.class);
    }

}

这是一个再平常不过的 SpringBoot 启动类, 我们进到 @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 {

这里就不全贴出来了, 只看一下注解就可以了 :

  • @SpringBootConfiguration : 如果点进去就可以发现, 里面存在 @Configuration 注解, 也就是说支持 JavaConfig 方式进行配置的
  • @EnableAutoConfiguration : 表示开启自动装配功能
  • @ComponentScan : 这个一定认识, 之前我们有讲过, 表示扫描注解

那么我们可以推断出 @SpringBootApplication 实际上等同于 @Configuration, @EnableAutoConfiguration, @ComponentScan

自动装配过程解析

一旦加上 @EnableAutoConfiguration 注解, 便表示将会开启自动装配功能, Spring 会试图在你的 classPath 下找到所有的配置 bean 然后进行装配, 根据若干个定制规则进行初始化, 源码贴一手

注意不同 SpringBoot 版本会存在不同区别, loger 这里使用的是 2.3.10.RELEASE

/**
 * Enable auto-configuration of the Spring Application Context, attempting to guess and
 * configure beans that you are likely to need. Auto-configuration classes are usually
 * applied based on your classpath and what beans you have defined. For example, if you
 * have {@code tomcat-embedded.jar} on your classpath you are likely to want a
 * {@link TomcatServletWebServerFactory} (unless you have defined your own
 * {@link ServletWebServerFactory} bean).
 * <p>
 * When using {@link SpringBootApplication @SpringBootApplication}, the auto-configuration
 * of the context is automatically enabled and adding this annotation has therefore no
 * additional effect.
 * <p>
 * Auto-configuration tries to be as intelligent as possible and will back-away as you
 * define more of your own configuration. You can always manually {@link #exclude()} any
 * configuration that you never want to apply (use {@link #excludeName()} if you don't
 * have access to them). You can also exclude them via the
 * {@code spring.autoconfigure.exclude} property. Auto-configuration is always applied
 * after user-defined beans have been registered.
 * <p>
 * The package of the class that is annotated with {@code @EnableAutoConfiguration},
 * usually via {@code @SpringBootApplication}, has specific significance and is often used
 * as a 'default'. For example, it will be used when scanning for {@code @Entity} classes.
 * It is generally recommended that you place {@code @EnableAutoConfiguration} (if you're
 * not using {@code @SpringBootApplication}) in a root package so that all sub-packages
 * and classes can be searched.
 * <p>
 * Auto-configuration classes are regular Spring {@link Configuration @Configuration}
 * beans. They are located using the {@link SpringFactoriesLoader} mechanism (keyed
 * against this class). Generally auto-configuration beans are
 * {@link Conditional @Conditional} beans (most often using
 * {@link ConditionalOnClass @ConditionalOnClass} and
 * {@link ConditionalOnMissingBean @ConditionalOnMissingBean} annotations).
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 1.0.0
 * @see ConditionalOnBean
 * @see ConditionalOnMissingBean
 * @see ConditionalOnClass
 * @see AutoConfigureAfter
 * @see SpringBootApplication
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	/**
	 * Environment property that can be used to override when auto-configuration is
	 * enabled.
	 */
	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

在里面表明了 @import AutoConfigurationImportSelector, 此类实现了 DeferredImportSelector 接口而接口又继承了 ImportSelector, 源码太长了这里就不贴出来了, 感兴趣的小伙伴可以用 idea 打开源码看一下, 简单来说主要是为了导入 @Configuration 里面的配置项, DeferredImportSelector 表示延期导入, 只有当所有 @Configuration 处理过后才会执行

我们要关注的是下面这个方法 AutoConfigurationImportSelector 的 selectImport 方法

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

此方法在开始时会先判断是否进行自动装配, 而后从 META-INF/spring-autoconfigure-metadata.properties 读取源数据与源数据相关的属性, 然后调用 getCandidateConfigurations 方法

/**
 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
 * of the importing {@link Configuration @Configuration} class.
 * @param annotationMetadata the annotation metadata of the configuration class
 * @return the auto-configurations that should be imported
 */
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	configurations = removeDuplicates(configurations);
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = getConfigurationClassFilter().filter(configurations);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

/**
 * Return the auto-configuration class names that should be considered. By default
 * this method will load candidates using {@link SpringFactoriesLoader} with
 * {@link #getSpringFactoriesLoaderFactoryClass()}.
 * @param metadata the source metadata
 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
 * attributes}
 * @return a list of candidate configurations
 */
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	List<String> configurations =SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
	Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
				+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
       String factoryTypeName = factoryType.getName();
       return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

SpringFactoryiesLoader 会读取 META-INF/spring.factories 下的 EnableAutoConfiguration 的配置, 紧接着在进行排除与过滤,进而得到需要装配的类。最后让所有配置在 META-INF/spring.factories 下的 AutoConfigurationImportListener 执行 AutoConfigurationImportEvent 事件

private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
	List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
	if (!listeners.isEmpty()) {
		AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
		for (AutoConfigurationImportListener listener : listeners) {
			invokeAwareMethods(listener);
			listener.onAutoConfigurationImportEvent(event);
		}
	}
}

protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
	return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class, this.beanClassLoader);
}

何时进行自动装配

在上述流程中只能知道最终要确定被装配的类, 那么 SpringBoot 何时进行自动装配呢 ?

哎, 有的同学可能 bean 的生命周期的学的比较好, 那么就猜对了, 还是 BPP (BeanFactoryPostProcessor)

我们先来看 BeanDefinitionRegistryPostProcessor 这个接口, 它继承了 BeanFactoryPostProcessor

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

再来看 ConfigurationClassPostProcessor 类

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware

它实现了 BeanDefinitionRegistryPostProcessor 那么也就可以认为简介实现了 BeanFactoryPostProcessor, 看一下关键代码

/**
 * Prepare the Configuration classes for servicing bean requests at runtime
 * by replacing them with CGLIB-enhanced subclasses.
 */
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	int factoryId = System.identityHashCode(beanFactory);
	if (this.factoriesPostProcessed.contains(factoryId)) {
		throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + beanFactory);
	}
	this.factoriesPostProcessed.add(factoryId);
	if (!this.registriesPostProcessed.contains(factoryId)) {
		// BeanDefinitionRegistryPostProcessor hook apparently not supported...
		// Simply call processConfigurationClasses lazily at this point then.
		processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
	}

	enhanceConfigurationClasses(beanFactory);
	beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

/**
 * Build and validate a configuration model based on the registry of
 * {@link Configuration} classes.
 */
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
	String[] candidateNames = registry.getBeanDefinitionNames();

	for (String beanName : candidateNames) {
		BeanDefinition beanDef = registry.getBeanDefinition(beanName);
		if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
			}
		} else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
			configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
		}
	}

	// Return immediately if no @Configuration classes were found
	if (configCandidates.isEmpty()) {
		return;
	}

	// Sort by previously determined @Order value, if applicable
	configCandidates.sort((bd1, bd2) -> {
		int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
		int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
		return Integer.compare(i1, i2);
	});

	// Detect any custom bean name generation strategy supplied through the enclosing application context
	SingletonBeanRegistry sbr = null;
	if (registry instanceof SingletonBeanRegistry) {
		sbr = (SingletonBeanRegistry) registry;
		if (!this.localBeanNameGeneratorSet) {
			BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
					AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
			if (generator != null) {
				this.componentScanBeanNameGenerator = generator;
				this.importBeanNameGenerator = generator;
			}
		}
	}

	if (this.environment == null) {
		this.environment = new StandardEnvironment();
	}

	// Parse each @Configuration class
	ConfigurationClassParser parser = new ConfigurationClassParser(
			this.metadataReaderFactory, this.problemReporter, this.environment,
			this.resourceLoader, this.componentScanBeanNameGenerator, registry);

	Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
	Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
	do {
		parser.parse(candidates);
		parser.validate();

		Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
		configClasses.removeAll(alreadyParsed);

		// Read the model and create bean definitions based on its content
		if (this.reader == null) {
			this.reader = new ConfigurationClassBeanDefinitionReader(
					registry, this.sourceExtractor, this.resourceLoader, this.environment,
					this.importBeanNameGenerator, parser.getImportRegistry());
		}
		this.reader.loadBeanDefinitions(configClasses);
		alreadyParsed.addAll(configClasses);

		candidates.clear();
		if (registry.getBeanDefinitionCount() > candidateNames.length) {
			String[] newCandidateNames = registry.getBeanDefinitionNames();
			Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
			Set<String> alreadyParsedClasses = new HashSet<>();
			for (ConfigurationClass configurationClass : alreadyParsed) {
				alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
			}
			for (String candidateName : newCandidateNames) {
				if (!oldCandidateNames.contains(candidateName)) {
					BeanDefinition bd = registry.getBeanDefinition(candidateName);
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
							!alreadyParsedClasses.contains(bd.getBeanClassName())) {
						candidates.add(new BeanDefinitionHolder(bd, candidateName));
					}
				}
			}
			candidateNames = newCandidateNames;
		}
	}
	while (!candidates.isEmpty());

	// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
	if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
		sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
	}

	if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
		// Clear cache in externally provided MetadataReaderFactory; this is a no-op
		// for a shared cache since it'll be cleared by the ApplicationContext.
		((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
	}
}

乱七八糟的写的是啥呢 ? 我们可以不用管注释已经写的很清楚了关键类是 ConfigurationClassParser, 我们可以继续向下追溯到类中的 parse 方法, 在其中的最后一段代码的 this.deferredImportSelectorHandler.process()方法, 老版本中应该是 processDeferredImportSelectors 方法, 会对 DeferredImportSelector 进行处理, 这样就可以与 AutoConfigurationSelectImporter 连到一起了

private class DeferredImportSelectorHandler {

	@Nullable
	private List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();

	/**
	 * Handle the specified {@link DeferredImportSelector}. If deferred import
	 * selectors are being collected, this registers this instance to the list. If
	 * they are being processed, the {@link DeferredImportSelector} is also processed
	 * immediately according to its {@link DeferredImportSelector.Group}.
	 * @param configClass the source configuration class
	 * @param importSelector the selector to handle
	 */
	public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
		DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
		if (this.deferredImportSelectors == null) {
			DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
			handler.register(holder);
			handler.processGroupImports();
		} else {
			this.deferredImportSelectors.add(holder);
		}
	}

	public void process() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		try {
			if (deferredImports != null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
				deferredImports.forEach(handler::register);
				handler.processGroupImports();
			}
		} finally {
			this.deferredImportSelectors = new ArrayList<>();
		}
	}
}

这是一个内部类, 在内部类内持有了一个 deferredImportSelectors 的引用, 至此将会执行自动装配的所有操作

小结

  • 自动装配利用了 SpringFactoriesLoader 来加载 META-INF/spring.factoires 文件里所有配置的 EnableAutoConfgruation,它会经过 exclude 和filter 等操作,最终确定要装配的类
  • 处理 @Configuration 的核心是 ConfigurationClassPostProcessor,这个类实现了 BeanFactoryPostProcessor, 因此当AbstractApplicationContext 执行 refresh 方法里的 invokeBeanFactoryPostProcessors(beanFactory) 方法时会执行自动装配

实现一个自动装配

原理也讲过了, 下面我们就来看一下怎么去实现自动装配, 实际上自动装配在实际工作中还是很有用的, 比如说可以将一些服务封装成为 'starter' 包, 哪里想用导哪里是吧, 看起来就非常高大上

首先我们来创建一个 'starter' 项目, pom 关键依赖如下, 版本的话兄弟们自己搞定, 我这边用的是 SpringBoot 2.3.10.RELEASE

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

然后创建一个 User 实体类

public class User {

    private String name;

    private String remark;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", remark='" + remark + '\'' +
                '}';
    }
}

然后创建 UserProperties

@ConfigurationProperties(prefix = "test")
public class UserProperties {

    private String name;

    private String remark;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }
}

然后我们再来创建 UserConfig

@EnableConfigurationProperties(value = UserProperties.class)
@Configuration
public class UserConfig {

    @Bean
    public User user(UserProperties userProperties){
        System.out.println("自动装配 begin ...");
        User user = new User();
        user.setName(userProperties.getName());
        user.setRemark(userProperties.getRemark());
        return user;
    }

}

这样我们的 'starter' 就搞定了, 如果你是 SpringBoot 项目的话可以将它打包然后引入到另一个项目里面, 这里 loger 用的是 SpringCloud 所以直接引入就可以了

建立一个测试项目, 配置 pom 文件如下, 将上边的 'starter' 导入进去

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>com.loger</groupId>
	<artifactId>TestDemo-Starter</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

我们在配置文件中进行属性的覆盖, 因为上面设置了匹配前缀 test, 所以如果你的配置是正确的这里应该会有提示

test:
  name: loger
  remark: 自动装配牛啊

用 junit 我们来测一下

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {UserConfig.class})
public class testConfig {

    @Autowired
    private User user;

    @Test
    public void testAll(){
        System.out.println(user);
    }

}

测试结果 :

自动装配测试结果.png

可以看到我们前面在 'starter' 包中打印的提示, 和配置文件中覆盖的属性全部都出来了, 至此自动装配讲解完毕

参考

结尾

如果大家自己搭建一个 SpringBoot 项目就可以感觉到 SpringBoot 是多么的方便, 而这么方便的重要原因之一就是它的自动装配功能, 既然 SpringBoot 可以应用自动装配完成 '傻瓜搭建模式', 在正常开发中, 如果有必要的话我们也可以沿用这种模式, 比如 loger 公司的短信服务, 就是通过自动装配实现的, 引入包后只需要在 yml 中配置短信服务商, 模板, 验签之类的东西就可以直接使用了

我是 loger 微信搜索 logerJava 关注公众号, 更多知识分享等你来看, 兄弟们记得点赞哦👍