详解 SpringBoot 自动装配
2024-03-28 10:32:53 # Technical # Spring

从 Spring 进入到 SpringBoot 有一个最直观的感受 —— 省去了大量 xml 的配置,一切看起来都是十分简洁高效的

什么是自动装配

现在提到自动装配都默认是 SpringBoot,其实 Spring 很早就实现了这个功能,SpringBoot 只是在其基础上,通过 SPI 的方式,做了进一步的优化

SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的 META-INF/spring.factories 文件,按照文件中配置的类信息加载到 Spring 容器中,并执行类中定义的各种操作。对于外部 jar 来说,只需按照 SpringBoot 定义的标准,就能将自己的功能装配进 SpringBoot

SpringBoot3.0 之后不再使用 SpringFactoriesLoader,而是 Spring 重新从 META-INFO/spring/ 目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中读取,其本质还是不变的,只是文件路径和文件名的改变

如何自动装配

既然是启动时自动装配的,先看启动时的核心注解 @SpringBootApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@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 {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};

@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};

@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};

@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};

@AliasFor(
annotation = ComponentScan.class,
attribute = "nameGenerator"
)
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

@SpringBootApplication 由三个注解组成:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan,其中 @SpringBootConfiguration 相当于是 @Configuration

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

三个关键注解的作用:

  • @Configuration:允许上下文中注册额外的 Bean 或导入其他配置类
  • @EnableAutoConfiguration:启用 SpringBoot 的自动装配
  • @ComponentScan:指定包扫描路径(默认为启动类所有包下),可指定基础包以及排除某些类

@EnableAutoConfiguration 工作原理

@EnableAutoConfiguration 是靠 AutoConfigurationImportSelector 进行解析加载的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 判断自动装配开关是否打开
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
// 获取所有需要装配的bean
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
String[] selectImports(AnnotationMetadata var1);
}

AutoConfigurationImportSelector 实现了 ImportSelector 接口的 selectImports 方法,方法里的关键在于 getAutoConfigurationEntry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
// 判断是否启用自动装配
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 获取EnableAutoConfiguration的属性:exclude和excludeName
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 读取META-INF/spring.factories
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 移除重复的的配置
configurations = this.removeDuplicates(configurations);
// 获取需要排除的自动配置类
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
// 检查需要排除的类是否在configurations中,如果不存在则抛出异常
this.checkExcludedClasses(configurations, exclusions);
// 从configurations中移除需要排除的自动配置类
configurations.removeAll(exclusions);
// 根据自动配置元数据进一步过滤configurations
configurations = this.filter(configurations, autoConfigurationMetadata);
// 触发自动配置导入事件,通知相关监听器
this.fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回一个包含最终自动配置列表和排除列表的自动配置条目
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

与 SPI 不同的是,SpringBoot 并不会将所有的配置都装载进来,装载的策略会按条件进行过滤,过滤方法在第 21 行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
long startTime = System.nanoTime();
// 从spring.factories中获取的自动配置类转出字符串数组
String[] candidates = StringUtils.toStringArray(configurations);
// 定义skip数组,是否需要跳过。注意skip数组与candidates数组顺序一一对应
boolean[] skip = new boolean[candidates.length];
boolean skipped = false;
// filters:OnBeanCondition,OnClassCondition和OnWebApplicationCondition
Iterator var6 = this.filters.iterator();

int i;
// 遍历这三个条件类去过滤从spring.factories加载的大量配置类
while(var6.hasNext()) {
// 注意candidates数组与match数组一一对应
AutoConfigurationImportFilter filter = (AutoConfigurationImportFilter)var6.next();
// 判断各种filter来判断每个candidate(这里实质要通过candidate(自动配置类)拿到其标注的
// @ConditionalOnClass,@ConditionalOnBean和@ConditionalOnWebApplication里面的注解值)是否匹配
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
// 遍历match数组,注意match顺序跟candidates的自动配置类一一对应
for (int i = 0; i < match.length; i++) {
// 若有不匹配的话
if (!match[i]) {
// 不匹配的将记录在skip数组,标志skip[i]为true,也与candidates数组一一对应
skip[i] = true;
// 因为不匹配,将相应的自动配置类置空
candidates[i] = null;
// 标注skipped为true
skipped = true;
}
}
}
// 这里表示若所有自动配置类经过OnBeanCondition,OnClassCondition和OnWebApplicationCondition过滤后,全部都匹配的话,则全部原样返回
if (!skipped) {
return configurations;
} else {
// 建立result集合来装匹配的自动配置类
List<String> result = new ArrayList<>(candidates.length);
for (int i = 0; i < candidates.length; i++) {
// 若skip[i]为false,则说明是符合条件的自动配置类,此时添加到result集合中
if (!skip[i]) {
result.add(candidates[i]);
}
}
// 打印日志
if (logger.isTraceEnabled()) {
int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered " + numberFiltered + " auto configuration class in "
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
+ " ms");
}
// 最后返回符合条件的自动配置类
return result;
}
}

这里的 filter 方法主要就是 通过 AutoConfigurationImportFilter 接口的 match 方法来判断每个自动配置类上面的条件注解(@ConditionOnClass@ConditionOnBean@ConditionOnWebApplication)是否满足

整体流程

SpringBoot 自动装配的整体流程:

  1. 利用 SPI 机制,从配置文件中读取需要自动配置的类
  2. 过滤 @EnableAutoConfigurationexclude 属性所要排除的类
  3. 再判断 @ConditionOnClass@ConditionOnBean@ConditionOnWebApplication 这三个条件注解是否满足条件
  4. 然后触发AutoConfigurationImportEvent事件,告诉ConditionEvaluationReport条件评估报告器对象来分别记录符合条件和exclude的自动配置类
  5. 最后将筛选后的自动配置类导入 IOC 容器中

创建一个自己的自动装配 Starter

基于 SpringBoot 3.X

模块

如果项目具有多种风格、选项或者可选特性,通常会定义两个模块 —— autoconfigurestarter

如果自动配置相对简单,没有可选特性,那么可以合并为一个 starter

命名

自动配置模块通常命名为:xxx-spring-boot-autoconfigure

starter 模块通常命名为:xxx-spring-boot-starter

配置参数

项目自定义的一些配置参数为确保命名空间的唯一,通常需要以项目名为开头

1
2
3
4
@ConfigurationProperties("xxx")
public class XxxProperties {
private String name = "xxx";
}

SpringBoot 内部针对配置的相关规则:

  • 不要使用「The」或者「A」开头
  • 对于布尔类型,用「Whether」或者「Enable」
  • 对于集合类型,使用逗号分隔的列表
  • 使用 java.time.Duration 而不是 long,并描述与毫秒不同的默认单位
  • 为确保配置的正确生成,需要检查生成的元数据 META-INF/spring-configuration-metadata.json

autoconfigure 模块

autoconfigure模块包含starter模块库所需的所有内容。它还可能包含配置键定义(例如@ConfigurationProperties)和任何回调接口,这些接口可以用于进一步定制组件的初始化方式

SpringBoot 使用一个注释处理器来收集元数据文件中自动配置的条件( META-INF/spring-autoconfiguration-metadata.properties)。如果该文件存在,它将用于主动过滤不匹配的自动配置,这将提高启动时间。因此建议在包含自动配置的模块中添加以下依赖项:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>

如果在应用程序中直接定义了自动配置,确保配置了 spring-boot-maven-plugin,以防止重新打包目标将依赖项添加到 fat.jar 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

starter 模块

starter 模块其实是一个空的 jar,它的唯一目的是提供使用库所需的依赖项

示例

开发一个模拟发送邮件的自定义 starter,最终的效果任何其它的项目只需引入 starter 配置基本的邮件配置,就可以使用 SendMailService 发送邮件。项目的结构 autoconfigurestarter 模块分开

1. 新建项目 email-spring-boot

设置顶级 pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<groupId>net.venom24</groupId>
<artifactId>email-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>

<name>email-spring-boot</name>
<description>模拟 email 发送 starter</description>

<modules>
<module>email-spring-boot-autoconfigure</module>
<module>email-spring-boot-starter</module>
</modules>

<!-- 打包方式为 pom -->
<packaging>pom</packaging>

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.0-M2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2. 新建模块 email-spring-boot-autoconfigure

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<parent>
<groupId>net.venom24</groupId>
<artifactId>email-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>email-spring-boot-autoconfigure</artifactId>
<packaging>jar</packaging>

<description>
autoconfigure
</description>

<dependencies>
<!--Spring Boot自动配置依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--元数据配置处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

添加配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Getter
@Setter
@ConfigurationProperties("email.service")
public class EmailProperties {
/**
* Enable of email service
*/
private boolean enable=true;
/**
* Host of the email.
*/
private String host;
/**
* Port of the email.
*/
private Integer port;
/**
* Name of the email.
*/
private String name;
/**
* Password of the email.
*/
private String password;
}

添加模拟邮件发送功能 EmailService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class EmailService {
private EmailProperties mailProperties;

public EmailService(EmailProperties mailProperties) {
this.mailProperties = mailProperties;
}

/**
* 发送邮件
*
* @param content 邮件发送内容
*/
public void send(String content) {
System.out.println("开始发送邮件:");
String info = "host:%s,port:%s";
System.out.println(String.format(info, mailProperties.getHost(), mailProperties.getPort()));
System.out.println("发送内容:" + content);
System.out.println("发送成功!");
}
}

添加自动配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@ConditionalOnClass(EmailService.class)
@EnableConfigurationProperties(value = EmailProperties.class)
public class EmailAutoConfiguration {
private final EmailProperties mailProperties;

public EmailAutoConfiguration(EmailProperties mailProperties) {
this.mailProperties = mailProperties;
}

@Bean
@ConditionalOnMissingBean(EmailService.class)
@ConditionalOnProperty(prefix = "email.service", value = "enable", havingValue = "true")
public EmailService mailService(EmailProperties mailProperties) {
return new EmailService(mailProperties);

}
}

简单解释下各个注解:

  • @Configuration:声明配置类
  • **@ConditionalOnClass(EmailService.class)**:只有在 classpath 中找到 EmailService 类的情况下才会解析这个自动配置类
  • **@EnableConfigurationProperties(value = EmailProperties.class)**:启用自动装配
  • @Bean:实例化一个 Bean
  • **@ConditionalOnMissingBean(EmailService.class)**:与 @Bean 配合使用,只有当 Spring 上下文中不存在 EmailService 时才会执行该实例化方法
  • **@ConditionalOnProperty(prefix = “email.service”, value = “enable”, havingValue = “true”)**: 当 classpath 中存在 EmailService 类时解析此配置类,并且在 Spring 上下文中没有 EmailService 的 bean 实例的情况下,new 一个实例出来,然后将应用配置中的相关配置值传入

添加自动装配文件

META-INF/spring/ 下新建 org.springframework.boot.autoconfigure.AutoConfiguration.imports

1
net.venom24.email.config.EmailAutoConfiguration

3. 新建模块 email-spring-boot-starter

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<parent>
<groupId>net.venom24</groupId>
<artifactId>email-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>email-spring-boot-starter</artifactId>
<packaging>jar</packaging>

<description>
starter
</description>

<dependencies>
<dependency>
<groupId>net.venom24</groupId>
<artifactId>email-spring-boot-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

email-spring-boot-starter 模块主要是集成 email-spring-boot-autoconfigure,以及一些其它的依赖项

4. 测试

新建一个测试项目,引入 email-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.example</groupId>
<artifactId>email-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

配置参数

1
2
3
4
5
email.service.enable=true
email.service.host=mail.qq.com
email.service.port=123
email.service.name=admin
email.service.password=admin

新建测试类测试

1
2
3
4
5
6
7
8
9
@SpringBootTest
class EmailStarterTestApplicationTests {
@Autowired
private EmailService emailService;
@Test
void contextLoads() {
emailService.send("我是自定义starter发送的邮件");
}
}

Output:

1
2
3
4
开始发送邮件
host:mail.qq.com,port:123
发送内容:我是自定义starter发送的邮件
发送成功!

SpringBoot 提供的条件注解

  • @ConditionalOnBean:当容器里有指定 Bean 的条件下
  • @ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
  • @ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于 SpEL 表达式作为判断条件
  • @ConditionalOnJava:基于 Java 版本作为判断条件
  • @ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
  • @ConditionalOnWebApplication:当前项目是 Web 项 目的条件下