SpringBoot 自动配置原理

我们知道 SpringBoot 的理念就是约定大于配置,这也使得我们在开发应用程序的过程更加便捷,以前的大量 XML 配置直接是噩梦呀,现在出现了 SpringBoot 明显降低了开发成本,而且大量的注解的使用帮我们省略掉了很多代码。本篇文章主要探究的是 SpringBoot 是如何实现自动配置并且如何加载配置 Bean 的,其实主要就是探究 @EnableAutoConfiguration 注解究竟发挥了怎样的作用。

@EnableAutoConfiguration

@SpringBootApplication 其实是三个注解的组合体,这三个注解中 @Configuration 和 @ComponentScan 对我们来说并不陌生,今天我们主要探究的是 @EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
@Target (ElementType.TYPE)
@Retention (RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import (AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}

其中最关键的要属 @Import (AutoConfigurationImportSelector.class),借助 AutoConfigurationImportSelector,@EnableAutoConfiguration 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器中。

借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持,@EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成!

mark

在 AutoConfigurationImportSelector 类中可以看到通过 SpringFactoriesLoader.loadFactoryNames ()
把 spring-boot-autoconfigure.jar/META-INF/spring.factories 中每一个 xxxAutoConfiguration 文件都加载到容器中,spring.factories 文件里每一个 xxxAutoConfiguration 文件一般都会有下面的条件注解。

  • @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 应用环境下起效

SpringFactoriesLoader

SpringFactoriesLoader 属于 Spring 框架私有的一种扩展方案(类似于 Java 的 SPI 方案 java.util.ServiceLoader),其主要功能就是从指定的配置文件 META-INF/spring-factories 加载配置,spring-factories 是一个典型的 java properties 文件,只不过 Key 和 Value 都是 Java 类型的完整类名,比如:

1
xpu.MyService = xpu.MyServiceImpl

对于 @EnableAutoConfiguration 来说,SpringFactoriesLoader 的用途稍微不同一些,其本意是为了提供 SPI 扩展的场景,而在 @EnableAutoConfiguration 场景中,它更多提供了一种配置查找的功能支持,即根据 @EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfig.EnableAutoConfiguration 作为查找的 Key,获得对应的一组 @Configuration 类。

SpringFactoriesLoader 是一个抽象类,类中定义的静态属性定义了其加载资源的路径 public static final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”,此外还有三个静态方法

  • loadFactories:加载指定的 factoryClass 并进行实例化。
  • loadFactoryNames:加载指定的 factoryClass 的名称集合。
  • instantiateFactory:对指定的 factoryClass 进行实例化。

mark

loadFactories 还是调用了 loadFactoryNames 与 instantiateFactory 方法。loadFactories 方法首先获取类加载器,然后调用 loadFactoryNames 方法获取所有的指定资源的名称集合、接着调用 instantiateFactory 方法实例化这些资源类并将其添加到 result 集合中。最后调用 AnnotationAwareOrderComparator.sort 方法进行集合的排序。

我们可以看一下 SpringBoot 的自动配置的 Bean 有哪些,见下图:

mark

自动配置示例

下面结合一个例子来加深理解,例子展示的是当项目启动时如果某个类存在就自动配置这个 Bean,并且这个属性可以在 application.properties 中配置。新建一个 Maven 项目,pom.xml 文件如下:

mark

pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>xpu.tim</groupId>
<artifactId>autoconfig-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.0.4.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package xpu.edu.tim;

public class Person {
private String name;

public String sayName(){
return "My Name is " + name;
}

public String getName() {
return name;
}

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

PersonProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package xpu.edu.tim;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties (prefix = "person") // 获取属性值
public class PersonProperties {
private static final String NAME = "Jock.Tim";

private String name = NAME ;

public String getName() {
return name;
}

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

PersonAutoConfiguration.java

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
package xpu.edu.tim;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
// 为带有 @ConfigurationProperties 注解的 Bean 提供有效的支持。
// 这个注解可以提供一种方便的方式来将带有 @ConfigurationProperties 注解的类注入为 Spring 容器的 Bean。
@EnableConfigurationProperties (PersonProperties.class)// 开启属性注入,通过 @autowired 注入
@ConditionalOnClass (Person.class)// 判断这个类是否在 classpath 中存在,如果存在,才会实例化一个 Bean
@ConditionalOnProperty (prefix="person", value="enabled", matchIfMissing = true)
public class PersonAutoConfiguration {
@Autowired
private PersonProperties personProperties;

@Bean
@ConditionalOnMissingBean (Person.class)// 容器中如果没有 Person 这个类,那么自动配置这个 Person
public Person person() {
Person person = new Person();
person.setName (personProperties.getName ());
return person;
}
}

spring.factories

1
org.springframework.boot.autoconfigure.EnableAutoConfiguration=xpu.edu.tim.PersonAutoConfiguration

最后使用 mvn package 将上面项目打包,使用 mvn install:install-file 命令将打包文件上传到本地 Maven 仓库进行测试,下面再新建一个 Maven 项目用于测试。

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
55
56
57
58
59
60
61
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>edu.tim</groupId>
<artifactId>spring-boot-start</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-start</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入上面的模块 -->
<dependency>
<groupId>xpu.tim</groupId>
<artifactId>autoconfig-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

mark

IndexController.java

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping ("/")
public class IndexController {
@Autowired
private Person person;

@GetMapping
public String index(){
return person.sayName ();
}
}

mark