Java · #java#spring-boot#auto-configuration

Spring Boot自动配置原理深度剖析

2025.01.08 Java 10 min 3.8k
// 目录 · contents

1. 引言

Spring Boot最为人称道的特性就是”约定优于配置”——只需引入一个starter依赖,相关的Bean就会被自动注册到Spring容器中,无需手动编写XML配置或大量的@Bean定义。那么这背后的魔法是如何实现的?

本文将从@SpringBootApplication注解出发,逐层拆解Spring Boot自动配置的完整链路,最后通过开发一个自定义Starter来将理论付诸实践。

2. @SpringBootApplication注解拆解

一切从主类上的@SpringBootApplication开始:

1
2
3
4
5
6
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

@SpringBootApplication是一个组合注解,包含三个核心注解:

graph TB
    SBA["@SpringBootApplication"] --> SC["@SpringBootConfiguration<br/>(本质是@Configuration)"]
    SBA --> EAC["@EnableAutoConfiguration<br/>自动配置的核心入口"]
    SBA --> CS["@ComponentScan<br/>包扫描"]

    EAC --> AI["@AutoConfigurationPackage<br/>注册主类所在包"]
    EAC --> ISR["@Import(AutoConfigurationImportSelector.class)<br/>加载自动配置类"]

    ISR --> SF["读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"]

    style SBA fill:#FF6347,color:#fff
    style EAC fill:#FFD700,stroke:#333,stroke-width:2px
    style ISR fill:#90EE90
    style SF fill:#87CEEB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @SpringBootApplication的定义(简化)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootConfiguration // 标识这是一个配置类
@EnableAutoConfiguration // 开启自动配置
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
// 可通过exclude/excludeName排除特定的自动配置类
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
Class<?>[] exclude() default {};

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

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

3. @EnableAutoConfiguration的工作原理

3.1 AutoConfigurationImportSelector

这是自动配置的核心引擎,它实现了DeferredImportSelector接口,负责筛选和加载自动配置类:

sequenceDiagram
    participant Spring as Spring容器启动
    participant Selector as AutoConfigurationImportSelector
    participant Loader as 配置文件加载器
    participant Filter as 条件过滤器

    Spring->>Selector: process()
    Selector->>Loader: 读取AutoConfiguration.imports
    Loader-->>Selector: 返回所有候选配置类(100+个)
    Selector->>Selector: 去重
    Selector->>Selector: 应用exclude排除
    Selector->>Filter: 应用@Conditional条件过滤
    Filter-->>Selector: 返回满足条件的配置类
    Selector-->>Spring: 返回最终要导入的配置类(通常20-30个)
    Spring->>Spring: 注册Bean定义

核心加载流程代码分析:

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
/**
* AutoConfigurationImportSelector核心方法(简化版)
* 完整源码位于spring-boot-autoconfigure模块
*/
public class AutoConfigurationImportSelector implements DeferredImportSelector {

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
// 获取所有候选自动配置类
AutoConfigurationEntry autoConfigurationEntry =
getAutoConfigurationEntry(annotationMetadata);
return autoConfigurationEntry.getConfigurations().toArray(new String[0]);
}

protected AutoConfigurationEntry getAutoConfigurationEntry(
AnnotationMetadata annotationMetadata) {

// 1. 从配置文件中加载所有候选自动配置类
List<String> configurations = getCandidateConfigurations(
annotationMetadata, null);
// 在Spring Boot 3.x中,约有150+个候选配置类

// 2. 去重
configurations = removeDuplicates(configurations);

// 3. 获取用户配置的排除列表
Set<String> exclusions = getExclusions(annotationMetadata, null);
configurations.removeAll(exclusions);

// 4. 应用配置类过滤器(基于@Conditional注解)
configurations = getConfigurationClassFilter().filter(configurations);

// 5. 触发AutoConfigurationImportEvent事件
fireAutoConfigurationImportEvents(configurations, exclusions);

return new AutoConfigurationEntry(configurations, exclusions);
}
}

3.2 配置文件的演变

Spring Boot加载自动配置类的配置文件经历了一次重要变化:

graph LR
    subgraph "Spring Boot 2.x"
        SF2["META-INF/spring.factories"]
        SF2 --> SF2C["org.springframework.boot.autoconfigure.EnableAutoConfiguration=\<br/>com.example.FooAutoConfiguration,\<br/>com.example.BarAutoConfiguration"]
    end
    subgraph "Spring Boot 3.x"
        SF3["META-INF/spring/<br/>org.springframework.boot.autoconfigure.AutoConfiguration.imports"]
        SF3 --> SF3C["com.example.FooAutoConfiguration<br/>com.example.BarAutoConfiguration<br/>(每行一个类名)"]
    end

    SF2 -->|"Spring Boot 2.7开始<br/>并行支持,3.0移除旧方式"| SF3

    style SF2 fill:#FFD700
    style SF3 fill:#90EE90

Spring Boot 2.x:使用META-INF/spring.factories文件,以key-value格式配置

1
2
3
4
# META-INF/spring.factories (Spring Boot 2.x)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfigure.FooAutoConfiguration,\
com.example.autoconfigure.BarAutoConfiguration

Spring Boot 3.x:使用新的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

1
2
3
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfigure.FooAutoConfiguration
com.example.autoconfigure.BarAutoConfiguration

4. @Conditional条件注解体系

自动配置的关键在于”按需加载”——只有满足特定条件时才生效。Spring Boot提供了丰富的@Conditional注解:

graph TB
    C["@Conditional<br/>(Spring Framework)"] --> CB["@ConditionalOnBean<br/>容器中存在指定Bean"]
    C --> CMB["@ConditionalOnMissingBean<br/>容器中不存在指定Bean"]
    C --> CC["@ConditionalOnClass<br/>classpath中存在指定类"]
    C --> CMC["@ConditionalOnMissingClass<br/>classpath中不存在指定类"]
    C --> CP["@ConditionalOnProperty<br/>配置属性满足条件"]
    C --> CWA["@ConditionalOnWebApplication<br/>是Web应用"]
    C --> CR["@ConditionalOnResource<br/>存在指定资源"]
    C --> CE["@ConditionalOnExpression<br/>SpEL表达式为true"]

    style C fill:#FFD700,stroke:#333,stroke-width:2px

DataSourceAutoConfiguration为例:

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
/**
* 数据源自动配置(简化版)
* 演示各种@Conditional注解的组合使用
*/
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
// 只有classpath中存在DataSource类时才生效
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
// 不存在R2DBC的ConnectionFactory时才生效(避免与响应式数据源冲突)
@EnableConfigurationProperties(DataSourceProperties.class)
// 绑定application.properties中spring.datasource.*前缀的配置
public class DataSourceAutoConfiguration {

@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
// 当没有配置外部数据源时,自动配置嵌入式数据库(如H2)
}

@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({
DataSourceConfiguration.Hikari.class, // HikariCP数据源
DataSourceConfiguration.Tomcat.class, // Tomcat JDBC数据源
DataSourceConfiguration.Dbcp2.class, // DBCP2数据源
})
protected static class PooledDataSourceConfiguration {
// 当classpath中存在连接池实现时,自动配置连接池数据源
}
}

4.1 @ConditionalOnMissingBean的重要性

这是自动配置中最常用的条件注解,它确保了”用户配置优先”的原则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 自动配置的核心设计模式:
* 先用@ConditionalOnMissingBean检查用户是否已自定义Bean
* 如果用户未自定义,才提供默认配置
*/
@Configuration
public class MyServiceAutoConfiguration {

@Bean
@ConditionalOnMissingBean // 用户已经定义了MyService Bean则跳过
public MyService myService() {
return new DefaultMyService(); // 提供默认实现
}
}

// 用户自定义配置 - 如果存在,则自动配置不生效
@Configuration
public class UserConfig {
@Bean
public MyService myService() {
return new CustomMyService(); // 用户自定义实现
}
}

5. 配置加载顺序

Spring Boot有严格的配置加载优先级,优先级高的会覆盖优先级低的:

graph TB
    P1["1. 命令行参数<br/>--server.port=8080"] --> P2
    P2["2. Java系统属性<br/>System.getProperties()"] --> P3
    P3["3. OS环境变量"] --> P4
    P4["4. application-{profile}.properties<br/>特定Profile配置"] --> P5
    P5["5. application.properties<br/>默认配置"] --> P6
    P6["6. @PropertySource注解"] --> P7
    P7["7. SpringApplication.setDefaultProperties()<br/>默认属性"]

    style P1 fill:#FF6347,color:#fff
    style P7 fill:#90EE90

    P1 ~~~ NOTE["优先级从高到低<br/>高优先级覆盖低优先级"]

配置文件的搜索路径(优先级从高到低):

1
2
3
4
5
1. file:./config/           # 项目根目录/config/
2. file:./config/*/ # 项目根目录/config/子目录/
3. file:./ # 项目根目录
4. classpath:/config/ # classpath下的config/
5. classpath:/ # classpath根目录

5.1 自动配置类的加载顺序控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 自动配置类之间可以通过注解控制加载顺序
*/
@AutoConfiguration(
after = DataSourceAutoConfiguration.class, // 在数据源配置之后
before = FlywayAutoConfiguration.class // 在Flyway配置之前
)
public class MyAutoConfiguration {
// 确保数据源已就绪后再配置
}

// Spring Boot 2.x中使用以下注解
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureBefore(FlywayAutoConfiguration.class)

6. 自定义Starter开发实战

6.1 项目结构

开发一个自定义Starter,通常由两个模块组成:

graph TB
    subgraph "my-spring-boot-starter"
        STARTER["my-service-spring-boot-starter<br/>(starter模块)"]
        AC["my-service-spring-boot-autoconfigure<br/>(autoconfigure模块)"]
        STARTER -->|依赖| AC
    end

    STARTER --> NOTE1["只包含pom.xml<br/>聚合依赖"]
    AC --> NOTE2["包含自动配置类<br/>@Conditional逻辑<br/>配置属性绑定"]

    style STARTER fill:#87CEEB
    style AC fill:#FFD700

命名规范: - 官方Starter:spring-boot-starter-{name}(如spring-boot-starter-web) - 第三方Starter:{name}-spring-boot-starter(如mybatis-spring-boot-starter

6.2 完整实现

下面实现一个短信发送服务的自定义Starter。

Step 1: 配置属性类

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
package com.example.sms.autoconfigure;

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

/**
* SMS服务配置属性
* 绑定application.properties中sms.*前缀的配置
*/
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
/**
* 是否启用SMS服务
*/
private boolean enabled = true;

/**
* SMS服务提供商:aliyun / tencent
*/
private String provider = "aliyun";

/**
* Access Key ID
*/
private String accessKeyId;

/**
* Access Key Secret
*/
private String accessKeySecret;

/**
* 短信签名
*/
private String signName;

/**
* 发送超时时间(毫秒)
*/
private int timeoutMs = 5000;

/**
* 最大重试次数
*/
private int maxRetries = 3;

// getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getProvider() { return provider; }
public void setProvider(String provider) { this.provider = provider; }
public String getAccessKeyId() { return accessKeyId; }
public void setAccessKeyId(String accessKeyId) { this.accessKeyId = accessKeyId; }
public String getAccessKeySecret() { return accessKeySecret; }
public void setAccessKeySecret(String accessKeySecret) { this.accessKeySecret = accessKeySecret; }
public String getSignName() { return signName; }
public void setSignName(String signName) { this.signName = signName; }
public int getTimeoutMs() { return timeoutMs; }
public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; }
public int getMaxRetries() { return maxRetries; }
public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
}

Step 2: 服务接口和实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.sms;

/**
* SMS发送服务接口
*/
public interface SmsService {
/**
* 发送短信
* @param phoneNumber 手机号
* @param templateCode 模板编码
* @param params 模板参数
* @return 是否发送成功
*/
boolean send(String phoneNumber, String templateCode, java.util.Map<String, String> params);
}
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
package com.example.sms;

import java.util.Map;

/**
* 阿里云SMS实现
*/
public class AliyunSmsService implements SmsService {
private final String accessKeyId;
private final String accessKeySecret;
private final String signName;
private final int timeoutMs;
private final int maxRetries;

public AliyunSmsService(String accessKeyId, String accessKeySecret,
String signName, int timeoutMs, int maxRetries) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.signName = signName;
this.timeoutMs = timeoutMs;
this.maxRetries = maxRetries;
}

@Override
public boolean send(String phoneNumber, String templateCode, Map<String, String> params) {
// 实际实现中调用阿里云SDK
System.out.printf("Aliyun SMS: sending to %s, template=%s, sign=%s, params=%s%n",
phoneNumber, templateCode, signName, params);
return true;
}
}
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
package com.example.sms;

import java.util.Map;

/**
* 腾讯云SMS实现
*/
public class TencentSmsService implements SmsService {
private final String accessKeyId;
private final String accessKeySecret;
private final String signName;

public TencentSmsService(String accessKeyId, String accessKeySecret, String signName) {
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.signName = signName;
}

@Override
public boolean send(String phoneNumber, String templateCode, Map<String, String> params) {
System.out.printf("Tencent SMS: sending to %s, template=%s, sign=%s, params=%s%n",
phoneNumber, templateCode, signName, params);
return true;
}
}

Step 3: 自动配置类

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
62
63
64
65
66
67
package com.example.sms.autoconfigure;

import com.example.sms.AliyunSmsService;
import com.example.sms.SmsService;
import com.example.sms.TencentSmsService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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;

/**
* SMS服务自动配置类
*
* 生效条件:
* 1. classpath中存在SmsService类
* 2. 配置了sms.enabled=true(默认true)
*/
@AutoConfiguration
@ConditionalOnClass(SmsService.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {

/**
* 阿里云SMS配置
* 当sms.provider=aliyun时生效
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "aliyun",
matchIfMissing = true)
static class AliyunSmsConfiguration {

@Bean
@ConditionalOnMissingBean(SmsService.class)
public SmsService aliyunSmsService(SmsProperties properties) {
return new AliyunSmsService(
properties.getAccessKeyId(),
properties.getAccessKeySecret(),
properties.getSignName(),
properties.getTimeoutMs(),
properties.getMaxRetries()
);
}
}

/**
* 腾讯云SMS配置
* 当sms.provider=tencent时生效
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "sms", name = "provider", havingValue = "tencent")
static class TencentSmsConfiguration {

@Bean
@ConditionalOnMissingBean(SmsService.class)
public SmsService tencentSmsService(SmsProperties properties) {
return new TencentSmsService(
properties.getAccessKeyId(),
properties.getAccessKeySecret(),
properties.getSignName()
);
}
}
}

Step 4: 注册自动配置

1
2
3
# 文件: META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# (Spring Boot 3.x)
com.example.sms.autoconfigure.SmsAutoConfiguration
1
2
3
4
# 文件: META-INF/spring.factories
# (Spring Boot 2.x兼容)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.autoconfigure.SmsAutoConfiguration

Step 5: 使用方配置

1
2
3
4
5
6
7
8
9
# application.yml
sms:
enabled: true
provider: aliyun
access-key-id: ${SMS_ACCESS_KEY_ID}
access-key-secret: ${SMS_ACCESS_KEY_SECRET}
sign-name: MyApp
timeout-ms: 3000
max-retries: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 使用方只需引入starter依赖,配置属性即可直接注入使用
*/
@RestController
@RequestMapping("/api/sms")
public class SmsController {

private final SmsService smsService;

public SmsController(SmsService smsService) {
this.smsService = smsService;
}

@PostMapping("/send")
public ResponseEntity<String> send(@RequestParam String phone,
@RequestParam String templateCode) {
boolean success = smsService.send(phone, templateCode,
java.util.Map.of("code", "123456"));
return success
? ResponseEntity.ok("sent")
: ResponseEntity.status(500).body("failed");
}
}

7. 调试自动配置

7.1 查看自动配置报告

1
2
3
4
5
6
# 方式1:启动参数
java -jar app.jar --debug

# 方式2:配置文件
# application.properties
debug=true

启动日志中会输出详细的自动配置报告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource',
'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'

Negative matches:
-----------------
MongoAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'com.mongodb.client.MongoClient'

7.2 排除特定自动配置

1
2
3
4
5
6
7
8
9
10
// 方式1:注解排除
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
RedisAutoConfiguration.class
})
public class MyApplication { }

// 方式2:配置文件排除
// application.properties
// spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

8. 最佳实践

  1. 自定义Bean优先于自动配置:始终使用@ConditionalOnMissingBean确保用户自定义配置能覆盖默认配置。
  2. 配置属性要有合理的默认值:让用户在最少配置的情况下就能使用。
  3. 使用@AutoConfiguration替代@Configuration:Spring Boot 3.x推荐使用专门的注解来标识自动配置类。
  4. 不要在自动配置类中使用@ComponentScan:避免意外扫描到用户的Bean。
  5. 提供配置元数据:创建META-INF/additional-spring-configuration-metadata.json,让IDE能提供配置提示。
  6. 合理使用@Conditional组合:精确控制配置生效条件,避免不必要的Bean创建。
  7. 注意自动配置的顺序:使用@AutoConfiguration(after/before)明确依赖关系。

9. 总结

Spring Boot自动配置的核心链路可以概括为:

  1. @SpringBootApplication -> @EnableAutoConfiguration -> AutoConfigurationImportSelector
  2. AutoConfigurationImportSelectorAutoConfiguration.imports文件中加载所有候选配置类
  3. 通过@Conditional系列注解进行条件过滤,只有满足条件的配置类才会生效
  4. 生效的配置类中定义的@Bean方法被执行,Bean被注册到Spring容器
  5. @ConditionalOnMissingBean确保了用户自定义配置始终优先于自动配置

理解这个链路后,不仅能更好地使用Spring Boot提供的自动配置,还能开发高质量的自定义Starter,将通用功能封装为可复用的组件,提升团队的开发效率。

作者 · authorzt
发布 · date2025-01-08
篇幅 · length3.8k 字 · 10 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论