前言
越向往光明,越要默默的扎根于黑暗
本篇文章已收录到 GitHub 仓库 github.com/logerJava/l…
在之前我们已经学习 Spring 中各式各样的知识点, 关于 Spring MVC 的话就不做多讲解了, 目前市场上除了一些老项目应该很少有地方会用 Spring MVC 框架了, 取而代之的是 SpringBoot, 就算是分布式项目 SpringCloud 中也是由一个个小的 SpringBoot 组成的, 本篇文章就来了解一下 loger 认为 SpringBoot 受欢迎的一个重要原因 - 自动装配
SpringBoot 自动装配原理
如果大家有自己搭建过 SpringBoot 项目, 一定会发现, 配置起来非常简单, 无需繁琐的配置文件也没有各种复杂的 pom 操作, 那么这么简便的原因是什么呢 ? 如果小伙伴们细心的话一定会发现, SpringBoot 的 pom 文件中有很多带有 'starter' 的包
这些就是关键, 我们来看一下要实现自动装配需要了解的三个关键注解
@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);
}
}
测试结果 :
可以看到我们前面在 'starter' 包中打印的提示, 和配置文件中覆盖的属性全部都出来了, 至此自动装配讲解完毕
参考
-
loger 公司研发部的组长讲解
-
深入理解 SpringBoot 自动装配 - 聂晨
结尾
如果大家自己搭建一个 SpringBoot 项目就可以感觉到 SpringBoot 是多么的方便, 而这么方便的重要原因之一就是它的自动装配功能, 既然 SpringBoot 可以应用自动装配完成 '傻瓜搭建模式', 在正常开发中, 如果有必要的话我们也可以沿用这种模式, 比如 loger 公司的短信服务, 就是通过自动装配实现的, 引入包后只需要在 yml 中配置短信服务商, 模板, 验签之类的东西就可以直接使用了
我是 loger 微信搜索 logerJava 关注公众号, 更多知识分享等你来看, 兄弟们记得点赞哦👍