Spring提供了IOC机制,基于此我们可以通过XML或者注解配置,将三方件注册到IOC中。问题是每个三方件都需要经过手动导入依赖、配置属性、注册IOC,比较繁琐。
基于"约定优于配置"原则的自动装配机制为该问题提供了一个解决方案。
不同SpringBoot版本细节部分存在差异,本文基于SpringBoot的2.3.2.RELEASE版本进行说明
SpringBoot在启动时通过SPI机制扫描所有JAR包下的spring.factories
文件,将文件中EnableAutoConfiguration包含的配置类全部加载到容器中。
根据各个配置类的条件确定是否进行装载,条件包括:容器中有无指定Bean,类路径中有无指定Class对象等。配置类内部Bean的定义也可通过条件确定是否进行装载。
Spring在spring-boot-autoconfigure包中为三方件定义了很多配置类,并提供了对应的starter依赖;用户只需通过引入对应的starter依赖即可完成对应三方件的组装。
以redis为例:
[1] 在pom.xml中添加redis对应的starter:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
[2] 在spring配置文件中添加对redis的配置:
spring:
redis:
host: localhost
port: 6379
timeout: 3000
database: 0
[3] 测试用例:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class RedisComponentTest {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
public void testRedis() {
Assert.assertEquals("testValue", redisTemplate.opsForValue().get("testKey"));
}
}
Note:在redis中添加"testKey" -> "testValue"后,该测试用例就可以运行成功。
原因分析:
在spring-boot-autoconfigure的spring.factories文件中有如下定义:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
...
进入RedisAutoConfiguration配置类:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {//...}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {//...}
}
RedisAutoConfiguration自动配置类的装配条件是@ConditionalOnClass(RedisOperations.class)
, 即类路径中包含RedisOperations.class. RedisOperations定义在spring-data-redis包中,而依赖的spring-boot-starter-data-redis包含了对spring-data-redis的依赖。
另外,在[SpringBoot系列-1 启动流程]中的也提到过使用jetty代替tomcat的方式:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jettyartifactId>
<version>2.6.4version>
dependency>
即类路径中删除了对Tomcat的默认依赖,添加了对Jetty的依赖;在自动配置类 ServletWebServerFactoryConfiguration中因找不到Tomcat.class对象而不会装配Tomcat相关组件,因引入了jetty的starter而装配Jetty容器。
除了SpringBoot自定义的starter外,也有第三方自定义的starter, 如常见的mybatis:
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.2version>
dependency>
用户也可基于SpringBoot提供的自动装配机制自定义starter,从而可以从多个项目中抽出重复的逻辑,以减少不必要的重复操作。本章通过一个完整的案例进行说明。
<groupId>com.demogroupId>
// [标注1]
<artifactId>demo-spring-boot-starterartifactId>
<version>1.0.0version>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
<version>2.7.5version>
dependency>
dependencies>
Note 1: springboot官方的starter依赖基本是pom, 用于关联需要的依赖项。而用户或者第三方自定义时,starter需要包含:配置类、依赖项、spring.factories文件。另外,命名时需要遵循命名规范:springboot定义的形式如spring-boot-starter-xxx, 用户自定义的形式如xxx-spring-boot-starter.
只需引入spring-boot-autoconfigure依赖即可,因为spring-boot-autoconfigure依赖了spring-boot,而spring-boot依赖了spring.
属性配置类用于提供用户自定义能力:
@ConfigurationProperties("demo.configure")
public class DemoProperties {
private String userName;
private String password;
// getter和setter方法
}
用户可以在spring.yml等配置文件中通过"demo.configure"对DemoProperties的userName和password属性进行配置。
服务类包含了该组件的核心逻辑:
public class DemoService {
private final DemoProperties demoProperties;
public DemoService(DemoProperties demoProperties) {
this.demoProperties = demoProperties;
}
public Boolean check(String name, String password) {
if (name == null || password == null) {
return false;
}
return name.equals(demoProperties.getUserName()) && password.equals(demoProperties.getPassword());
}
}
此时提供了一个服务方法,校验用户名和密码。
@Configuration
//导入属性配置类
@EnableConfigurationProperties(DemoProperties.class)
@ConditionalOnClass(DemoService.class)
public class DemoAutoConfiguration {
@Bean
public DemoService demoService(DemoProperties demoProperties) {
return new DemoService(demoProperties);
}
}
添加了@ConditionalOnClass(DemoService.class)表示当DemoService.class在类路径中时,该自动装配类才会生效。
在resources目录下新增META-INF/spring.factories文件,指定自动配置类:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.caltta.demo.DemoAutoConfiguration
将上述的starter项目install到仓库后,在其他项目中可通过如下方式引入:
<dependency>
<groupId>com.calttagroupId>
<artifactId>demo-spring-boot-starterartifactId>
<version>1.0.0version>
dependency>
在application.yml文件中配置:
demo:
configure:
userName: root
password: Root.123
测试用例:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemoApplication.class})
public class DemoComponentTest {
@Autowired
private DemoService demoService;
@Test
public void testDemoService() {
Assert.assertTrue(demoService.check("root", "Root.123"));
}
}
测试用例可正常运行。
@SpringBootApplication注解是由@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan注解组成的复合注解:
(1) @SpringBootConfiguration本质上是一个@Configuration注解;
(2) @ComponentScan定义了包扫描路径;
(3) @EnableAutoConfiguration开启自动装配。
@SpringBootApplication注解中定义了几个属性:
(1) scanBasePackages/scanBasePackageClasses
桥接给@ComponentScan,用于确定扫描包路径,默认我注解类所在路径;
(2) exclude/excludeName
桥接给@EnableAutoConfiguration,用于排除自动装配的类;
(3) proxyBeanMethods
桥接给@Configuration注解,用于确定代理类型。
@EnableAutoConfiguration由@AutoConfigurationPackage
和@Import(AutoConfigurationImportSelector.class)
组成:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
其中,@AutoConfigurationPackage
注解用于向IOC添加一个BasePackages类型的Bean对象,属性默认为注解所在类的包名。
@Import(AutoConfigurationImportSelector.class)
用于向容器导入AutoConfigurationImportSelector对象,该部分是整个装配机制的关键。
AutoConfigurationImportSelector是DeferredImportSelector接口的实现类,更是ImportSelector接口的实现类。
selectImports
方法如下:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断是否开启自动装配
if (!isEnabled(annotationMetadata)) {
return {};
}
// 获取&&返回需要装配的类型列表
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
Note: 在ConfigurationClassPostProcessor处理@Import注解时,对于DeferredImportSelector类型调用的是getAutoConfigurationEntry
方法。
上述逻辑住就要包含两个方法:
isEnabled
方法表示是否开启自动装配,逻辑如下:
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true);
}
return true;
}
Note:以"spring.boot.enableautoconfiguration"为key从环境变量中取值,如果为false则关闭自动装配,其他情况(空或者false)开启。如:在application.yml中配置“spring.boot.enableautoconfiguration”值为false即可关闭。
getAutoConfigurationEntry
方法用于获取待装配的类:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
// 获取注解属性,即@EnableAutoConfiguration的exclude和excludeName
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 根据SPI机制从spring.factories中加载EnableAutoConfiguration的值
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 去重,因为spring.factories文件加载自多个jar包-可能有重复
configurations = removeDuplicates(configurations);
// 根据@EnableAutoConfiguration的exclude和excludeName进行排除
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 对初步排除的结果进行再次过滤
configurations = getConfigurationClassFilter().filter(configurations);
// 发送事件&&返回结果
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
getExclusions
方法获取需要排除的装配类:
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
// 从环境变量中"spring.autoconfigure.exclude"指定的类型数组
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
Note: 排除自动装配可通过@EnableAutoConfiguration的exclude和excludeName属性,也可通过在application.yml中设置"spring.autoconfigure.exclude"值来进行排除。
getConfigurationClassFilter().filter(configurations)
方法对候选的自动装配类进行再一次过滤。
getConfigurationClassFilter()
获取配置自动配置过滤器的主要逻辑如下:
private ConfigurationClassFilter getConfigurationClassFilter() {
//...
List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
//...
}
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
}
Note-1: 获取过滤器:
从spring.factories文件中获取AutoConfigurationImportFilter对应的值。spring-boot-autoconfigure包中的spring.factories文件中有如下定义:
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
默认情况下(无用户自定义&&三方件引入),只有OnBeanCondition、OnClassCondition、OnWebApplicationCondition三个过滤器。该过滤器与自动装配的元数据配合实现快速排除不必要的自动配置类,加快容器启动速度。
Note-2: 构造ConfigurationClassFilter
new ConfigurationClassFilter(this.beanClassLoader, filters)
方法构造时,传入了过滤器,同时从类路径加载了元数据:
ConfigurationClassFilter(ClassLoader classLoader, List<AutoConfigurationImportFilter> filters) {
// 加载"META-INF/spring-autoconfigure-metadata.properties"文件内容
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);
this.filters = filters;
}
Note-3: 执行过滤
List<String> filter(List<String> configurations) {
String[] candidates = StringUtils.toStringArray(configurations);
boolean skipped = false;
for (AutoConfigurationImportFilter filter : this.filters) {
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {
return configurations;
}
List<String> result = new ArrayList<>(candidates.length);
for (String candidate : candidates) {
if (candidate != null) {
result.add(candidate);
}
}
return result;
}
逻辑较为清晰:对每个候选的自动配置类都进行三个过滤器的过滤操作(调用过滤器的match方法),只有三个过滤器都返回true才会保留;否则会被标记为false,然后排除。skipped用于优化流程,没有匹配失败情况,可快速退出。
遍历过滤器调用filter.match(candidates, this.autoConfigurationMetadata)
方法,以OnClassCondition为例进行说明。
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
// 省略日志...
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
boolean[] match = new boolean[outcomes.length];
for (int i = 0; i < outcomes.length; i++) {
match[i] = (outcomes[i] == null || outcomes[i].isMatch());
}
return match;
}
入参中: autoConfigurationClasses表示候选的自动装配类列表,autoConfigurationMetadata表示加载的自动配置元数据。
getOutcomes
方法根据autoConfigurationMetadata对每个候选的自动装配类生成一个匹配结果,结果为空或者true表示匹配,继续看getOutcomes
方法实现细节:
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
} else {
OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0, autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
return outcomesResolver.resolveOutcomes();
}
}
根据处理器个数进行优化,确定是否折成两半分别进行,本质还是调用了StandardOutcomesResolver的resolveOutcomes
方法:
public ConditionOutcome[] resolveOutcomes() {
return getOutcomes(this.autoConfigurationClasses, this.start, this.end, this.autoConfigurationMetadata);
}
private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[end - start];
for (int i = start; i < end; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
// 从元数据中获取ConditionalOnClass为key尾缀的值
String candidates = autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnClass");
if (candidates != null) {
outcomes[i - start] = getOutcome(candidates);
}
}
}
return outcomes;
}
Note:
在spring-boot-autoconfigure包中定义的spring-autoconfigure-metadata.properties文件有如下定义:
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.ConditionalOnClass=\
org.springframework.data.redis.core.RedisOperations
表示此阶段会根据类路径中是否存在RedisOperations类确定是否排除自动配置类RedisAutoConfiguration。
继续跟踪getOutcome(candidates)
方法进入:
private ConditionOutcome getOutcome(String className, ClassLoader classLoader) {
// 会根据类加载机制是否排除异常,确定类是否存在
if (ClassNameFilter.MISSING.matches(className, classLoader)) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class).didNotFind("required class").items(Style.QUOTE, className));
}
return null;
}
ClassNameFilter.MISSING的matches
方法实现如下:
public boolean matches(String className, ClassLoader classLoader) {
return !isPresent(className, classLoader);
}
static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
// 使用类加载器加载className
resolve(className, classLoader);
return true;
} catch (Throwable ex) {
return false;
}
}
isPresent
方法通过类加载器去类路径中加载,加载成功则返回true,否则返回false.
上述为OnClassCondition过滤机制。
对于一个SpringBoot项目,已经知道了自动装配机制的实现原理;现在再结合@Configuration注解分章节梳理一下Bean的注入IOC的流程。
这部分需要读者对Spring启动流程和ConfigurationClassPostProcessor和SpringBoot启动流程有比较清晰的理解,可参考:Spring系列-11 @Configuration注解原理 和 SpringBoot系列-1启动流程和 Spring系列-1 启动流程.
为表述方便,使用SpringBoot系列-1启动流程中案例进行介绍,如下所示:
@SpringBootApplication
// 标注[1]
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
SpringBoot系列-1启动流程的章节-2.2中介绍:在SpringApplication对象的run
方法中,刷新Spring容器前的准备阶段中通过BeanDefinitionLoader将主配置类导入IOC中,即此时主配置类DemoApplication已被导入IOC容器。
配置类值被@Configuration注解的Bean。ConfigurationClassPostProcessor是BeanDefinitionRegistryPostProcessor接口的实现类,更是BeanFactoryPostProcessor接口实现类,因此在容器刷新阶段会通过invokeBeanFactoryPostProcessors
方法调用其勾子方法(时机比注入非懒加载靠前)。
调用勾子逻辑进入ConfigurationClassPostProcessor类的processConfigBeanDefinitions
方法:
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) {
} 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);
});
// ...
// ⚠️第二阶段...
}
processConfigBeanDefinitions
方法可以分为两个阶段:
(1) 第一阶段:从IOC容器中获取配置类;
(2) 第二阶段:解析配置类获取Bean对象,并讲所有的Bean对象注入到IOC中.
实际上,此时获取的configCandidates获取的就是 章节-4.1 主配置类注入阶段 中注入IOC的DemoApplication.
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// ⚠️第一阶段...
// ⚠️第二阶段:
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);
this.reader.loadBeanDefinitions(configClasses);
// ...
}
while (!candidates.isEmpty());
// ...
}
该阶段可以分为两个步骤:解析出所有的Bean、注册解析得到的Bean。核心逻辑在于前者,配置类解析依赖于解析器ConfigurationClassParser,存在递归逻辑,用图解表示如下:
DemoApplication类上注解了@SpringBootApplication,继而间接注解了@Import(AutoConfigurationImportSelector.class),在4.3 解析配置类阶段会通过ImportSelect逻辑导入AutoConfigurationImportSelector类,从而启动自动装配过程。
Note: 上图中的条件过滤用于处理注解在自动配置类中添加的@Conditional注解。
条件注解的解析和判断在ConditionEvaluator类的shouldSkip
中方法进行,读者可自行阅读。