SpringBoot源码解析之AutoConfiguration

自动配置绝对算得上是Spring Boot的最大亮点,完美的展示了CoC约定优于配置; 所以对其如何实现的探究可以为我们平时的工作和实践中带来一些参考和灵感,并且在实际使用SpringBoot减少所谓灵异事件的产生。

1. 概述

SpringBoot的Auto-configuration的核心实现都位于spring-boot-autoconfigure-xxx.jar;其中SpringBoot将依据classpath里面的依赖内容来自动配置bean到IOC容器,Auto-configuration会尝试推断哪些beans是用户可能会需要的。比如如果HSQLDB包在当前classpath下,并且用户并没有配置其他数据库链接,这时候Auto-configuration功能会自动注入一个基于内存的数据库连接到应用的IOC容器。

在上一篇文章 SpringBoot源码研究之Start中我们已经探究了
@SpringBootApplication = @EnableAutoConfiguration + @ComponentScan + @Configuration ; 而其中的@EnableAutoConfiguration 则正是实现Auto Config的关键之所在。

这里首先提一句,本次测试代码中,我们在程序启动入口传递给SpringApplication.run的类,其类名为AutoConfigSpringBootApplication;因为下面会多次出现它的名称,所以这里先提及一下,防止读者出现认知偏差或者误解。

2. 关键类AutoConfigurationImportSelector

@EnableAutoConfiguration 注解会导致AutoConfigurationImportSelector类的实例被引入到Spring容器中,而该类的继承链如下:

SpringBoot源码解析之AutoConfiguration_第1张图片

因此对于 AutoConfigurationImportSelector类, 我们重点关注的是其实现自ImportSelector接口的方法selectImports,而直接继承的DeferredImportSelector作为一个标志性接口,主要作用是为了Defer(推迟;延期;)

在我们继续探索之前,让我们暂停一下,先来回顾下Spring是如何执行到这里来的,即如何调用到AutoConfigurationImportSelector.selectImports方法的。

SpringBoot源码解析之AutoConfiguration_第2张图片

有关ConfigurationClassPostProcessor的相关内容可以参见本人之前的博客Spring源码研究之@Configuration的第五小节进行了解。上述堆栈中我们可以看到 ConfigurationClassParser.parse() 被调用(关于ConfigurationClassParser 类的解释则一并出现在上述博客的第七小节),而其参数candidates ,作为一个集合参数其中只包含我们在启动SpringBoot时传入的那个AutoConfigSpringBootApplication类包裹所形成的BeanDefinitionHolder实例。这一段逻辑正是我在上述博客中舍去了的部分,而它也是本次我们所关注的重点。

ConfigurationClassParser.parse(Set configCandidates)方法最终会调用到自身内部私有的processDeferredImportSelectors()方法:

// 本方法位于 protected 访问级别的 ConfigurationClassParser 中
private void processDeferredImportSelectors() {
	// @EnableAutoConfiguration注解上修饰的@Import(AutoConfigurationImportSelector.class) 注解的解析是由 ConfigurationClassParser.parse中开始调度完成(本类中的processImports方法), 进而载入到本类的 deferredImportSelectors 字段中。
	// 这里要特别注意,正因为AutoConfigurationImportSelector是一个DeferredImportSelector实例,所以其生效时机晚于@Import生效的时机,这也使得逻辑时序可以正确地运行下去。
	List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
	this.deferredImportSelectors = null;
	Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
	
	for (DeferredImportSelectorHolder deferredImport : deferredImports) {
		// 这里取到的configClass就是我们自定义的 AutoConfigSpringBootApplication
		ConfigurationClass configClass = deferredImport.getConfigurationClass();
		try {
			// 核心逻辑就是下面这两句了
			// 首先是这行, 负责回调我们上面使用@Import导入的AutoConfigurationImportSelector里的逻辑实现, 详情将在本文接下来的内容
			// 最终的返回值是经过筛选,满足要求的类名
			String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
			// configClass 就是我们传递给 SpringApplication.run 的AutoConfigSpringBootApplication类
			// 该方法最终会跳转到 本类内部的doProcessConfigurationClass方法中,来将相应Bean注册进容器, Auto Config完成。
			processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
		}
		catch  {
			// 异常处理略
		}
	}
}

绕了一圈终于回到本小节原本关注的内容——有关AutoConfigurationImportSelector实现的selectImports方法。

// AutoConfigurationImportSelector (位于package - org.springframework.boot.autoconfigure, 所以是SpringBoot自带的)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	try {
		// 加载 META-INF/spring-autoconfigure-metadata.properties 中的相关配置信息, 注意这主要是供Spring内部使用的
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		// 获取所有通过META-INF/spring.factories配置的, 此时还不会进行过滤和筛选
		// KEY为 : org.springframework.boot.autoconfigure.EnableAutoConfiguration
		// 有兴趣的读者可以去看看spring-boot-autoconfigure-xxxx.RELEASE.jar中看看,这里我就不骗字数了
		List<String> configurations = getCandidateConfigurations(annotationMetadata,
				attributes);
		// 开始对上面取到的进行过滤,去重,排序等操作
		configurations = removeDuplicates(configurations);
		configurations = sort(configurations, autoConfigurationMetadata);
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
		configurations = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		// 这里返回的满足条件, 通过筛选的配置类 
		return configurations.toArray(new String[configurations.size()]);
	}
	catch (IOException ex) {
		throw new IllegalStateException(ex);
	}
}

以上正是Springboot完成Auto Config功能的关键点之一了。在本实现中,SpringBoot只是告知Spring需要去加载(Import)哪些Config类,剩下的工作依然是Spring那已经经过千锤百炼的逻辑来完成; 这正是 “微核 + 扩展”的优秀架构设计经验的极致体现。

另外还要专门感概下,以上相关代码中的命名和排版堪称经典,近乎完美展示了《Clean Code》中的绝大部分规范。

3. package org.springframework.boot.autoconfigure.condition

上面我们讨论的AutoConfigurationImportSelector只能告诉Spring哪些类需要加载,但判断所配置的类是否可以被加载(即Auto Config里的Auto)是一个非常繁琐的逻辑,如果由某个中央控制系统来处理的话,必然会造成代码耦合和复杂性猛增,因此SpringBoot最终使用了一贯的做法——将判断是否加载的权限下放给了各个需要进行自动配置的需求方本身,思维的转换有时候就是柳暗花明又一村。

基于Spring的@Conditional,SpringBoot提供了丰富的条件配置

@ConditionalOnClass : classpath中存在该类时起效 
@ConditionalOnMissingClass : classpath中不存在该类时起效 
@ConditionalOnBean : DI容器中存在该类型Bean时起效 
@ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效 
@ConditionalOnSingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效 
@ConditionalOnExpression : SpEL表达式结果为true时 
@ConditionalOnProperty : 参数设置或者值一致时起效 
@ConditionalOnResource : 指定的文件存在时起效 
@ConditionalOnJndi : 指定的JNDI存在时起效 
@ConditionalOnJava : 指定的Java版本存在时起效 
@ConditionalOnWebApplication : Web应用环境下起效 
@ConditionalOnNotWebApplication : 非Web应用环境下起效

@AutoConfigureAfter:在指定的配置类初始化后再加载 
@AutoConfigureBefore:在指定的配置类初始化前加载 
@AutoConfigureOrder:数越小越先初始化

以上便是SpringBoot所提供的部分条件加载的注解;下面让我们用一个实际的例子来结束本小节

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
// 诚如上面所说, Web应用环境下将向Spring容器中注入本类的实例
@ConditionalOnWebApplication
// 该类为内部类, 作用是确保Spring容器中某些bean的存在
@Import(BeanPostProcessorsRegistrar.class)
public class EmbeddedServletContainerAutoConfiguration {

	// 下面的注释解释的已经很清晰了
	/**
	 * Nested configuration if Tomcat is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Tomcat.class })
	// 查找策略是当前容器内
	@ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedTomcat {

		@Bean
		public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
			return new TomcatEmbeddedServletContainerFactory();
		}

	}

	...
}

这里要注意如下一个细节:

  1. org.springframework.boot.autoconfigure.condition.OnClassCondition通过实现AutoConfigurationImportFilter以及配置文件spring-boot-autconfig-xxx.jar/META-INF/spring.factories,实现了在配置文件加载阶段(源码位置:AutoConfigurationImportSelector.selectImports)就将@ConditionalOnClass@ConditionalOnMissingClass 进行了一次筛选。
  2. 而其他诸如@ConditionalOnExpression 则依然是交给了Spring的原有逻辑(通过接口Condition来实现)。而其他诸如@ConditionalOnExpression 则依然是交给了Spring的原有逻辑(通过接口Condition来实现)。

4. 范例

上面扯了那么多理论和源码,接下来就让我们来点实际的——自定义一个Auto Config。

4.1 自定义类

首先是相关的自定义类, 唯一需要注意的是以下这些类最好独立于SpringBoot的扫描目录之外,避免不必要的诡异问题。(虽然SpringBoot已经尽可能地避免这种情况造成的问题——例如其通过@SpringBootApplication引入的AutoConfigurationExcludeFilter类,就是为了防止类似情况【即作为@Configuration,又被spring.factories引入一次】的出现。)

@Configuration
// 这个LqConfig仅仅是一个可以被SpringBoot扫描到的类, 其中没有任何实现
@ConditionalOnClass({ LqConfig.class })
// 在配置文件中发现有配置  spring.lq.auto=true 即启用本配置类
@ConditionalOnProperty(prefix = "spring.lq", name = "auto", havingValue = "true", matchIfMissing = true)
public class LqAutoConfig {
	static{
		System.out.println("LQ static");
	}
	
	public LqAutoConfig(){
		System.out.println("LQ construct");
	}
}

// --------------------------------------------------------------------------------------------------------------
// 生效位置: `AutoConfigurationImportSelector.getAutoConfigurationImportFilters`。
// 范例为   spring-boot-autconfig-xxx.jar中META-INF/spring.factories里配置的 OnClassCondition
public final class LqAutoConfigurationImportFilterImpl implements AutoConfigurationImportFilter {
	 
	@Override
	public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
		boolean[] match = new boolean[autoConfigurationClasses.length];
		for (int i = 0; i < autoConfigurationClasses.length; i++) {
			String autoConfigurationClass = autoConfigurationClasses[i];
			match[i] = !autoConfigurationClass.contains("Lq");
		}
		return match;
	}

}
4.2 相关配置

在classpath下创建 META-INF/spring.factories ,并在其中填入以下内容:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xx.springboot.autoconf.LqAutoConfig,\
# 下面这个AopAutoConfiguration是专门用来测试SpringBoot的去重;可忽略
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
com.xx.springboot.autoconfig.LqAutoConfigurationImportFilterImpl
4.3 效果

通过以上一通忙活你就会发现,除去过程,最终呈现在你面前的,没有任何不同。这是因为我们所配置EnableAutoConfiguration配置类LqAutoConfig,被同样由我们所配置的AutoConfigurationImportFilter配置类LqAutoConfigurationImportFilterImpl给过滤掉了,所以最终是竹篮打水一场空。

4.4 另外一个例子

最后我们再举一例,以Mybatis的扩展插件Mybatis-plus为例:

  1. 其对SpringBoot AutoConfig的支持来自于:
    <dependency>
    	<groupId>com.baomidougroupId>
    	<artifactId>mybatisplus-spring-boot-starterartifactId>
    	<version>1.0.5version>
    dependency>	
    
  2. 而在mybatisplus-spring-boot-starter-1.0.5.jar中,其META-INF/spring.factories内容如下:
    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusAutoConfiguration
    
  3. 所以重点就是这个MybatisPlusAutoConfiguration类了。

5. 细节

  1. org.springframework.boot.autoconfigure.condition.OnClassCondition类,在其getOutcomes()方法的实现中,将任务进行了均分,然后进行并行计算最后合并结果,这一点在其源码中进行了相应的注释,这个思路值得我们借鉴。
  2. spring-boot-autoconfigure-xxx.jar 还提供了AutoConfigurationReportLoggingInitializer类(默认被配置在 spring-boot-autoconfigure-xxx.jar/META-INF/spring.factories中),用来提供自动配置结果的汇报功能;如果对自动配置结果有疑问的话,可以在配置文件中使用debug=true开启此功能进行验证。
  3. Spring默认情况下只会扫描你指定的路径/类下的内容,所以如果某个JAR中的类被诸如@Configuration修饰,默认也是不会被扫描进Spring容器的。这一点上鄙人之前有点想当然了,不过这也是很合理的。也正基于此,SpringBoot需要提供AutoConfigurationImportSelector来告知Spring容器需要扫描哪些内容(经典的微核+扩展),并且spring-boot-autoconfigure-xxx.jar中如此多被标注了注解的类没有在不需要的情况下进入Spring容器。Spring-Session中的RedisHttpSessionConfiguration 正是应用到了这个特性,简化了相关依赖类的导入复杂度。
  4. SpringBoot还提供了AutoConfigurationImportFilter接口来让使用者介入Auto Config的引入逻辑;通过使用该接口,用户可以介入并扭转SpringBoot默认的Auto Config逻辑规则;不过该接口也是需要被配置在META/spring.factories中,生效位置: AutoConfigurationImportSelector.getAutoConfigurationImportFilters;而范例则是 spring-boot-autconfig-xxx.jar的配置文件所配置的OnClassCondition。
  5. 关于本文中的最大幕后功臣DeferredImportSelector标记性接口,它本身没有定义任何方法,且直到Spring4.0才被作为ImportSelector(Spring3.1)的子类正式引入。该接口的生效时机位于所有@Configuration都已经被解析完毕之后,而且相应的实现类可以通过实现Ordered接口,来定义多个DeferredImportSelector的优先级别;该接口在需要筛选的引入类型具备@Conditional注解的时候非常有用(be particularly useful when the selected imports are @Conditional)。

6. 总结

得益于Spring强大的微核+扩展的强大设计底蕴,SpringBoot借助Spring4.0引入的DeferredImportSelector接口,通过向Spring容器注入DeferredImportSelector接口实现类AutoConfigurationImportSelector(告知Spring容器应该加载哪些@Configuration类,其中的逻辑使用到了后面将要提到的注解等),结合同样4.0引入的@ConditionalCondition等一系列相关辅助类(这些注解可以约定自身是否可以被注入到Spring容器中)来完成自动配置功能。

没有新的功能性加入,但这并不妨碍其伟大之处。

7. Links

  1. auto-configuration ~ Office Site
  2. Spring Boot 入门 - 自动配置(AutoConfigure)
  3. Spring Boot实现自动配置的原理
  4. Spring源码研究之注解扫描context:component-scan/ – 有关@SpringBootApplication引入的两个@Filter可参见本链接

你可能感兴趣的:(Spring,SpringBoot)