Java 基础体系 · 第 25/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。
Spring Boot 工程基础:自动配置、Starter、配置绑定、Actuator 和启动
Spring Boot 不是另一个取代 Spring Framework 的容器。它建立在 Spring Framework 的 IoC 容器、Bean 生命周期、事件机制、AOP、事务代理和 Web 能力之上,主要解决三个工程问题:
- 如何根据 classpath 和配置自动装配一组合理的 Bean;
- 如何以可复用的依赖组合引入技术能力;
- 如何让应用具备可配置、可观测、可启动和可部署的工程结构。
本文以 Java 25 LTS 为目标运行环境。具体 Spring Boot 版本必须以其官方系统要求为准:不同 Boot 小版本对 Java 版本的支持范围可能不同,不能仅因为本机安装了 Java 25 就假设任意旧版本都支持它。Maven、IDE 和 CI 中应统一使用兼容的 JDK,并将编译目标设为 25。
一、从 Spring Framework 到 Spring Boot
1. IoC 容器是 Boot 的运行基础
Spring Framework 的 IoC(Inversion of Control,控制反转)容器负责创建和管理对象。被容器管理的对象称为 Bean。
传统代码通常直接创建依赖:
public final class OrderService {
private final PaymentClient paymentClient =
new PaymentClient("https://payment.example");
public void createOrder() {
paymentClient.pay();
}
}
这里 OrderService 决定了 PaymentClient 的具体实现、构造方式和配置来源。若测试时想替换为假的客户端,就必须修改代码或增加复杂的工厂逻辑。
使用 Spring 后,依赖关系可以交给容器:
@Component
public final class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
容器启动时大致经历以下过程:
- 扫描组件或读取配置类;
- 注册 BeanDefinition;
- 解析构造器依赖;
- 创建
PaymentClient; - 创建
OrderService并注入依赖; - 执行 Bean 后处理器、初始化回调和相关事件。
Spring Boot 并没有改变这个基本模型。它做的是:在合适的条件下,替应用注册大量常见的 BeanDefinition,并提供默认配置。
例如,当 classpath 中存在 Spring MVC、Servlet Web 容器和 JSON 序列化库时,Boot 可以自动提供:
DispatcherServlet;- MVC 基础设施;
- JSON
HttpMessageConverter; - 内嵌 Web 服务器;
- 错误处理相关组件。
这些 Bean 最终仍然由 Spring IoC 容器管理。
2. @SpringBootApplication 做了什么
典型入口类如下:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication 是一个组合注解,核心上等价于:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
三部分职责不同:
@SpringBootConfiguration表示该类是 Boot 应用的配置入口,通常内部使用@Configuration;@EnableAutoConfiguration启用自动配置;@ComponentScan扫描当前包及其子包中的组件。
因此,入口类所在包的位置很重要。若入口类放在:
com.example
而组件位于:
com.example.order
com.example.user
默认扫描可以发现它们。若入口类误放在 com.example.bootstrap.internal,则位于同级或父级的组件可能不会被扫描。
这不是自动配置失效,而是组件扫描范围不包含目标类。
二、自动配置:条件满足时提供默认 Bean
1. 自动配置的定义
自动配置(Auto-configuration)是 Spring Boot 根据应用的:
- classpath;
- 已注册的 Bean;
- 配置属性;
- Web 应用类型;
- 环境和条件注解;
决定是否导入某些配置类,并在条件满足时注册默认 Bean 的机制。
它不是“扫描所有依赖后无条件创建对象”,而是一个条件化的 Bean 注册过程。
一个简化的自动配置类可能写成:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisClient redisClient(RedisProperties properties) {
return new RedisClient(properties.getUrl());
}
}
这个配置类表达了两个条件:
- classpath 中存在
RedisClient; - 容器中没有用户自己提供的
RedisClientBean。
只有两个条件都满足时,默认 Bean 才会注册。
2. 自动配置的导入过程
现代 Spring Boot 自动配置通常通过自动配置元数据导入机制发现候选配置类。自动配置模块的资源目录中一般包含:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件内容列出自动配置类的全限定名:
com.example.redis.RedisAutoConfiguration
Boot 启动时会读取这些候选项,然后交给条件评估机制处理。
早期 Spring Boot 版本曾广泛使用:
META-INF/spring.factories
因此,编写与当前 Boot 版本匹配的自动配置模块时,应以对应版本官方文档为准,不能将旧的注册方式无条件套用于新版本。
自动配置类本身常使用 @AutoConfiguration,也可以使用带有相应元数据的配置类。核心并不在注解名字,而在于:
- 候选配置被发现;
- 条件被评估;
- 满足条件则导入;
- 配置类中的 Bean 定义进入 IoC 容器;
- 用户定义的 Bean 通常通过条件覆盖默认 Bean。
3. 常见条件及其逻辑
常见条件注解可以理解为布尔条件:
@ConditionalOnClass:某个类必须存在于 classpath;@ConditionalOnMissingClass:某个类不能存在;@ConditionalOnBean:容器中必须已有某类 Bean;@ConditionalOnMissingBean:容器中不能已有某类 Bean;@ConditionalOnProperty:某个配置属性满足指定条件;@ConditionalOnWebApplication:当前应用是 Web 应用;@ConditionalOnNotWebApplication:当前应用不是 Web 应用;@ConditionalOnResource:某个资源存在;@ConditionalOnExpression:表达式满足条件。
例如:
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
prefix = "feature.audit",
name = "enabled",
havingValue = "true",
matchIfMissing = false
)
public class AuditAutoConfiguration {
@Bean
AuditService auditService() {
return new AuditService();
}
}
配置为:
feature:
audit:
enabled: true
则条件成立;不配置或配置为 false,该 Bean 不会注册。
@ConditionalOnMissingBean 体现了 Boot 的扩展边界:
@Bean
@ConditionalOnMissingBean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
它的意思不是“禁止用户定义同名 Bean”,而是“仅当容器中没有符合条件的 Bean 时,才提供默认 Bean”。用户可以通过自己的配置类注册 Bean,从而接管默认实现。
4. 自动配置的实际判定过程
假设某个数据库自动配置包含以下条件:
C = HasDriver
AND HasDataSourceClass
AND PropertyEnabled
AND MissingUserDataSource
应用启动时可能得到:
HasDriver = true
HasDataSourceClass = true
PropertyEnabled = true
MissingUserDataSource = false
C = true AND true AND true AND false
= false
最终自动配置不会创建默认数据源。此时“数据库依赖已引入但没有自动创建数据源”并不矛盾,通常是因为用户已经注册了同类型 Bean。
如果结果不是预期,可以使用:
java -jar app.jar --debug
启动日志会输出条件评估报告,通常包含:
- Positive matches:匹配成功;
- Negative matches:匹配失败;
- Unconditional classes:无条件导入的配置。
也可以提高相关包的日志级别:
logging:
level:
org.springframework.boot.autoconfigure: DEBUG
诊断时应先问三个问题:
- 目标类是否真的在运行时 classpath 中;
- 属性名和值是否符合条件;
- 是否已经存在一个使
@ConditionalOnMissingBean失效的 Bean。
只看“我在 pom.xml 中声明了依赖”是不够的,因为编译期依赖、运行时依赖和最终打包内容可能不同。
5. 自动配置不是无限制的隐式魔法
自动配置有明确边界:
- 它只能根据可观察的 classpath、Bean 和配置进行判断;
- 它不能理解业务意图;
- 多个候选实现存在时,可能需要
@Primary或@Qualifier; - 用户手动注册 Bean 后,某些默认配置会被条件排除;
- 条件顺序和配置类导入关系会影响结果。
可以显式排除某个自动配置:
@SpringBootApplication(
exclude = DataSourceAutoConfiguration.class
)
public class DemoApplication {
}
也可以通过配置排除:
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
排除自动配置不是修复配置错误的常规手段。若应用本来需要数据源,排除后可能只是把启动失败推迟到业务首次访问数据库时。
三、Starter:依赖组合,而不是运行时魔法
1. Starter 的定义
Starter 是一种约定化的依赖聚合模块,通常命名为:
spring-boot-starter-xxx
例如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
它的主要作用是通过传递依赖引入一组技术栈所需的库,例如:
- Spring Framework Web MVC;
- JSON 序列化库;
- 校验库;
- 内嵌 Servlet 容器;
- Boot 基础模块。
Starter 本身通常不是“启动器线程”,也不是一个运行时容器。它首先是 Maven 或 Gradle 的依赖集合。
需要区分两个概念:
| 概念 | 作用 |
|---|---|
| Starter | 聚合依赖,解决“需要哪些库” |
| Auto-configuration | 条件化注册 Bean,解决“这些库如何接入容器” |
一个技术能力通常由两者共同提供:
starter
└── 引入基础库和自动配置模块
└── 自动配置读取 classpath 和属性
└── 注册 Spring Bean
如果只引入一个底层库,可能有类可用,但没有 Boot 自动配置;如果只引入自动配置模块,却缺少它依赖的核心库,条件通常不会满足或应用无法运行。
2. 一个最小 Web 工程
下面是 Maven 依赖结构示意。${spring-boot.version} 必须替换为官方文档中支持 Java 25 的 Spring Boot 版本,并使用与该版本匹配的 parent 或 dependency management。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>boot-foundation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
构建和启动:
mvn clean package
java -jar target/boot-foundation-0.0.1-SNAPSHOT.jar
预期现象包括:
Started DemoApplication in ... seconds
端口默认是 8080。若控制器映射了 /hello,访问:
curl http://localhost:8080/hello
即可获得响应。
查看依赖树:
mvn dependency:tree
这个命令用于验证 Starter 实际引入了什么。若发生版本冲突,应观察:
- 同一库是否出现多个版本;
- 哪个依赖路径引入了旧版本;
- 最终打包出的 JAR 是否包含预期库。
不要仅仅通过手工添加某个传递依赖的更高版本来“解决”冲突。某个 Boot 自动配置可能依赖特定 API,强行升级后会在启动或运行时出现 NoSuchMethodError、ClassNotFoundException 等二进制兼容问题。
3. 自定义 Starter 的组成
若公司内部有统一的审计、链路或客户端接入能力,可以构造两个模块:
company-audit-spring-boot-autoconfigure
company-audit-spring-boot-starter
其中:
autoconfigure包含配置类、条件和@ConfigurationProperties;starter通常只负责聚合autoconfigure、核心客户端和必要依赖。
自动配置示例:
@AutoConfiguration
@EnableConfigurationProperties(AuditProperties.class)
@ConditionalOnClass(AuditClient.class)
@ConditionalOnProperty(
prefix = "company.audit",
name = "enabled",
havingValue = "true"
)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
AuditClient auditClient(AuditProperties properties) {
return new AuditClient(properties.endpoint());
}
}
对应的注册资源:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容:
com.company.audit.AuditAutoConfiguration
这样,业务应用只需依赖:
<dependency>
<groupId>com.company</groupId>
<artifactId>company-audit-spring-boot-starter</artifactId>
</dependency>
而无需在每个项目中重复编写 Bean 配置。
自定义 Starter 的失败路径很典型:
- 忘记注册
AutoConfiguration.imports:配置类根本不会被发现; @ConditionalOnClass写了不存在的类:自动配置被跳过;- 属性前缀不一致:绑定值为空;
- 没有
@ConditionalOnMissingBean:用户无法自然替换默认实现; - 把业务代码放进 Starter:依赖边界变得不可控。
四、配置绑定:把外部配置变成类型化对象
1. 属性来源与环境模型
Spring Boot 的配置最终进入 Spring Environment。配置来源可以包括:
application.properties;application.yaml;- profile 专用配置文件;
- 操作系统环境变量;
- JVM 系统属性;
- 命令行参数;
- 外部配置文件等。
多个来源存在时,优先级决定最终值。命令行参数通常可以覆盖配置文件中的同名属性:
java -jar app.jar --server.port=9090
这会使服务器端口变为 9090,即使配置文件写的是:
server:
port: 8080
生产排查时应确认“最终 Environment 中的值”,而不是只查看仓库里的配置文件。
2. @Value 与 @ConfigurationProperties
少量单值注入可以使用:
@Value("${app.name}")
private String appName;
但当配置具有层级、列表、校验和复用需求时,更适合使用 @ConfigurationProperties。
配置文件:
app:
client:
base-url: https://api.example.com
connect-timeout: 2s
read-timeout: 5s
retry:
max-attempts: 3
backoff: 200ms
Java 配置类:
@ConfigurationProperties(prefix = "app.client")
@Validated
public class ClientProperties {
@NotBlank
private String baseUrl;
@NotNull
private Duration connectTimeout = Duration.ofSeconds(2);
@NotNull
private Duration readTimeout = Duration.ofSeconds(5);
@Valid
private Retry retry = new Retry();
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public Duration getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Duration getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
public Retry getRetry() {
return retry;
}
public void setRetry(Retry retry) {
this.retry = retry;
}
public static class Retry {
private int maxAttempts = 3;
private Duration backoff = Duration.ofMillis(200);
public int getMaxAttempts() {
return maxAttempts;
}
public void setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
}
public Duration getBackoff() {
return backoff;
}
public void setBackoff(Duration backoff) {
this.backoff = backoff;
}
}
}
让容器发现它:
@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
绑定过程可以简化为:
Environment
└── app.client.base-url
└── app.client.connect-timeout
└── app.client.retry.max-attempts
↓
Binder 按前缀匹配并转换类型
↓
ClientProperties Bean
2s 会转换为 Duration.ofSeconds(2),200ms 会转换为 Duration.ofMillis(200)。这种类型化配置比在业务代码中反复解析字符串更安全。
3. 宽松绑定不是任意拼写都有效
Boot 支持一定程度的宽松命名,例如 Java 属性:
private String baseUrl;
通常可以绑定以下形式:
app.client.base-url
app.client.baseUrl
APP_CLIENT_BASEURL
但这不意味着任意缩写、下划线或大小写组合都可靠。配置键应使用规范形式:
app:
client:
base-url: https://api.example.com
环境变量通常使用大写和下划线:
export APP_CLIENT_BASE_URL=https://api.example.com
配置前缀应使用小写字母、数字和连字符构成的规范形式,避免把 Java 类名风格直接写进前缀。
4. 构造器绑定与不可变配置
配置对象也可以采用不可变形式:
@ConfigurationProperties("app.client")
public record ClientProperties(
@NotBlank String baseUrl,
Duration connectTimeout,
Duration readTimeout
) {
}
这种形式的优点是:
- 配置对象创建后不能被业务代码随意修改;
- 必填字段在构造阶段获得;
- 依赖关系更清晰。
但记录类型需要使用与当前 Spring Boot 版本匹配的配置绑定规则,并正确启用扫描或显式注册。若使用较复杂的嵌套结构,普通 JavaBean 形式更容易调试。
5. 配置校验的失败表现
若配置为:
app:
client:
base-url: ""
而 baseUrl 标记了 @NotBlank,应用启动时可能在绑定阶段失败,错误通常包含:
- 绑定失败的前缀;
- 具体属性名;
- 违反的校验约束;
- 建议修复的信息。
这是理想的失败位置:应用在启动阶段拒绝一个明确无效的配置,而不是启动成功后第一次调用远程服务才失败。
常见误区是只在配置类上写 @Validated,却忘记对嵌套对象使用 @Valid,导致嵌套字段的校验没有按预期级联。另一个误区是使用 @Value 拼接大量配置,使错误信息只剩下一条难以定位的占位符解析异常。
五、Actuator:把应用内部状态暴露为运维接口
1. Actuator 的作用
Spring Boot Actuator 是一组生产运维能力,主要提供:
- 健康检查;
- 指标;
- 应用信息;
- Bean、环境和配置诊断;
- 启动时间和步骤信息;
- HTTP 追踪等能力。
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Actuator 端点(endpoint)是可被调用的管理操作或观察入口,例如:
/actuator/health
/actuator/info
/actuator/metrics
/actuator/beans
2. 启用、暴露和访问不是同一件事
Actuator 中有三个容易混淆的概念:
- 端点是否启用:端点 Bean 是否创建;
- 端点是否暴露:是否通过 HTTP、JMX 等方式提供;
- 端点是否允许访问:是否通过认证和授权。
例如:
management:
endpoints:
web:
exposure:
include: health,info,metrics
这表示通过 HTTP 暴露三个端点。默认暴露范围通常比较保守,不能假设所有端点都会自动公开。
查看健康状态:
curl http://localhost:8080/actuator/health
常见结果:
{"status":"UP"}
如果存在数据库、消息队列等健康指示器,响应可能包含组件详情;生产环境应谨慎决定是否公开这些细节,因为组件名称、连接状态和错误信息可能泄露内部结构。
可以将管理端口与业务端口分离:
server:
port: 8080
management:
server:
port: 8081
endpoints:
web:
exposure:
include: health,info,metrics
此时:
curl http://localhost:8081/actuator/health
访问管理端口,而业务请求仍然使用 8080。
端口分离不是完整安全方案。若 8081 仍暴露在不受控网络中,仍需网络策略、认证和授权。
3. 健康检查和就绪状态
健康检查回答的是“应用或依赖当前是否适合继续提供服务”。
在容器环境中,常见区分是:
- liveness:进程是否陷入不可恢复状态;
- readiness:应用是否已准备好接收流量。
若启用探针相关配置,可以使用:
management:
endpoint:
health:
probes:
enabled: true
随后检查相应的健康组端点。具体端点和可用组取决于 Boot 版本及运行环境,应以该版本文档为准。
错误的健康检查设计会导致故障扩大。例如:
- 将一个短暂的第三方超时直接作为 liveness 失败;
- liveness 失败触发容器重启;
- 重启增加请求抖动;
- 更多实例同时重启,最终形成级联故障。
通常,外部依赖不可用更适合影响 readiness,而不是直接判定进程已经不可恢复。
4. 指标不是日志的替代品
访问:
curl http://localhost:8080/actuator/metrics
通常会得到可用指标名称列表。再查询某个指标:
curl http://localhost:8080/actuator/metrics/jvm.memory.used
响应会包含当前值及标签。指标适合回答:
- 请求数量是否增加;
- 延迟分布是否恶化;
- JVM 内存是否持续增长;
- 线程池是否饱和。
日志适合回答:
- 某个请求为什么失败;
- 哪个业务参数触发了异常;
- 某次启动加载了什么配置。
二者观察维度不同。Actuator 提供数据出口,但不会自动替代指标存储系统、告警规则或日志平台。
5. 端点暴露的风险
以下端点可能包含高敏感信息:
env:环境属性;configprops:配置绑定结果;beans:容器 Bean 结构;mappings:请求映射;heapdump:内存转储;loggers:运行时日志配置。
不应为了“方便排查”而暴露全部端点:
management:
endpoints:
web:
exposure:
include: "*"
这可能让攻击者发现数据库地址、内部服务名、请求路由甚至敏感配置。即使敏感值被掩码,也不能把掩码机制当作访问控制。
生产环境至少应:
- 只暴露必需端点;
- 限制管理网络;
- 对管理接口进行认证和授权;
- 对健康详情按访问者权限控制;
- 在临时开启诊断端点后恢复配置并验证。
六、Spring Boot 启动:从 main 到可接收请求
1. 启动主流程
调用:
SpringApplication.run(DemoApplication.class, args);
可以抽象为以下阶段:
sequenceDiagram
participant M as main
participant A as SpringApplication
participant E as Environment
participant C as ApplicationContext
participant B as BeanFactory
participant S as Embedded Server
participant R as Runner
M->>A: run(args)
A->>E: 加载配置和命令行参数
A->>C: 创建并准备 ApplicationContext
A->>C: 注册主配置类
C->>B: 解析组件与自动配置
B->>B: 创建 Bean、注入依赖、执行生命周期回调
C->>S: 刷新 Web 上下文并启动服务器
C-->>A: 发布应用已启动事件
A->>R: 执行 CommandLineRunner/ApplicationRunner
A-->>M: 返回已运行的 Context
不同 Boot 版本和 Web 类型会有实现细节差异,但重要的因果关系是:
- 配置先进入 Environment;
- 自动配置和组件扫描形成 Bean 定义;
refresh期间创建 Bean;- Web 应用创建并启动嵌入式服务器;
- 应用进入可服务状态;
- Runner 在启动阶段执行。
2. refresh 为什么关键
Spring ApplicationContext 的刷新过程会触发大量基础设施动作:
- 注册 BeanFactory 后处理器;
- 注册 Bean 后处理器;
- 实例化非懒加载单例;
- 解析依赖;
- 执行
@PostConstruct等初始化回调; - 发布上下文刷新事件。
如果某个必需 Bean 创建失败,刷新过程可能失败,应用整体启动失败。常见错误包括:
NoSuchBeanDefinitionException
BeanCurrentlyInCreationException
UnsatisfiedDependencyException
BindException
这些错误分别可能表示:
- 容器中没有所需类型的 Bean;
- 发生循环创建;
- 依赖链中的某个 Bean 无法满足;
- 端口已被占用或绑定失败。
启动失败时,应用不应被认为“基本可用”。即使某个非关键线程已经启动,只要上下文刷新未完成,进程通常会退出或进入失败状态。
3. Bean 生命周期和启动事件
一个单例 Bean 的简化生命周期如下:
实例化
↓
依赖注入
↓
Aware 回调
↓
BeanPostProcessor 前置处理
↓
@PostConstruct / InitializingBean / init-method
↓
BeanPostProcessor 后置处理
↓
可被业务使用
↓
容器关闭时执行销毁回调
因此,不应在构造器中依赖尚未初始化完成的其他状态,也不应把耗时远程调用随意放进所有 Bean 的初始化阶段。启动阶段的阻塞会直接延长应用进入就绪状态的时间。
启动回调可以这样写:
@Component
public class StartupCheck implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
System.out.println("application startup check completed");
}
}
CommandLineRunner 接收原始字符串参数:
@Component
public class CommandRunner implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println(Arrays.toString(args));
}
}
ApplicationRunner 提供解析后的 ApplicationArguments。两者都在启动阶段执行,适合校验参数、预热本地数据或执行明确的一次性初始化,不适合无限等待的任务。
如果多个 Runner 有顺序要求,可以使用 @Order 或实现排序接口。但排序只能解决 Runner 之间的先后,不能替代对 Bean 依赖关系的建模。
4. 关闭流程和优雅停机
收到关闭信号后,Spring 容器会进入关闭流程:
- 停止接收新的请求,具体行为由 Web 服务器和部署方式共同决定;
- 执行生命周期组件的停止逻辑;
- 调用 Bean 的销毁回调;
- 关闭线程池、连接池等资源。
例如:
@Component
public class ResourceHolder {
@PreDestroy
public void close() {
System.out.println("release resources");
}
}
若销毁逻辑阻塞,进程可能无法在平台规定的终止时间内退出,最终被强制杀死。因此资源释放操作应有边界和超时意识。
Spring Boot 支持优雅停机相关配置,但实际效果取决于:
- 使用的嵌入式服务器;
- 容器编排平台的终止信号和宽限期;
- 负载均衡器摘流速度;
- 应用自身请求处理时间。
不能只配置应用参数而忽略 Kubernetes、systemd 或云平台的停止策略。
七、启动性能与启动诊断
1. Startup 记录
Boot 可以记录启动步骤,用于定位启动慢的阶段。典型方式是配置 BufferingApplicationStartup:
public static void main(String[] args) {
SpringApplication app = new SpringApplication(DemoApplication.class);
app.setApplicationStartup(
new BufferingApplicationStartup(2048)
);
app.run(args);
}
随后可以通过相关 Actuator 能力观察启动步骤,具体端点暴露方式取决于 Boot 版本和配置。
启动耗时不能只看总时间。应拆分为:
JVM 启动
+ 类加载
+ Spring Context 创建
+ 自动配置条件评估
+ Bean 实例化
+ 数据库连接
+ Web 服务器启动
+ Runner 执行
例如,总启动时间增加可能不是 Spring 本身变慢,而是某个 @PostConstruct 中新增了远程 HTTP 调用。
2. 常见启动失败的定位路径
端口冲突
配置:
server:
port: 8080
启动时若端口已占用,通常会出现绑定失败信息。Linux 上可以检查:
ss -ltnp | grep 8080
修复路径是:
- 确认占用者是否为旧实例;
- 若旧实例应退出,先正常停止;
- 若端口确实需要共存,修改
server.port; - 重新启动并用
curl验证实际端口。
不要直接杀掉未知进程,因为它可能是另一个生产服务。
配置绑定失败
看到类似:
Failed to bind properties under 'app.client'
应检查:
- 前缀是否一致;
- 属性名称是否符合绑定规则;
- 字符串是否能转换为目标类型;
- 是否违反 Bean Validation;
- 实际生效的 profile 和配置文件是什么。
循环依赖
如果 A 依赖 B,B 又依赖 A:
A -> B -> A
构造器注入通常会在创建阶段直接失败。不要把打开循环依赖开关作为首选修复,因为它可能把结构问题隐藏在代理或延迟引用之后。应重新划分职责,或者引入明确的第三方协调对象。
Web 类型不符合预期
引入 spring-boot-starter-web 通常创建 Servlet Web 应用;引入 spring-boot-starter-webflux 则倾向于 Reactor/WebFlux 模型。但当两者同时存在时,实际 Web 应用类型和自动配置结果受 Boot 的判定规则、显式配置及依赖关系影响。
Servlet MVC 与 WebFlux 的选择不是简单的“哪个性能更高”:
- MVC 以 Servlet 请求模型和线程处理为基础;
- WebFlux 以 Reactor Publisher 和非阻塞链路为基础;
- 在 WebFlux 中调用阻塞 JDBC、文件 IO 或同步 HTTP 客户端,仍然会阻塞线程;
- 在 MVC 中使用 Reactor 类型,并不会自动使整个调用链变成非阻塞。
选择应基于上下游客户端、数据库驱动、团队调试能力和请求模型,而不是只看 Starter 名称。
八、Starter、自动配置和用户代码的完整协作示例
下面用一个简单的客户端功能串起完整链路。
1. 配置文件
app:
client:
base-url: https://api.example.com
connect-timeout: 2s
read-timeout: 5s
management:
endpoints:
web:
exposure:
include: health,info,metrics
2. 配置绑定
@ConfigurationProperties("app.client")
public record ClientProperties(
String baseUrl,
Duration connectTimeout,
Duration readTimeout
) {
}
3. 注册配置属性
@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
4. 使用配置创建客户端
@Configuration(proxyBeanMethods = false)
public class ClientConfiguration {
@Bean
HttpClient httpClient(ClientProperties properties) {
return HttpClient.newBuilder()
.connectTimeout(properties.connectTimeout())
.build();
}
}
这里的因果链是:
YAML 文本
↓
Environment
↓
Binder
↓
ClientProperties
↓
HttpClient Bean
↓
业务服务通过构造器注入使用
如果 base-url 写错,绑定对象可能仍然创建成功,因为 String 本身可接受很多内容;若希望启动时拒绝非法 URL,应增加显式校验或在创建客户端时进行 URI 解析。配置绑定成功不等于业务语义一定正确。
5. 暴露一个 MVC 接口
@RestController
class HelloController {
private final ClientProperties properties;
HelloController(ClientProperties properties) {
this.properties = properties;
}
@GetMapping("/hello")
Map<String, Object> hello() {
return Map.of(
"service", "boot-foundation",
"remoteBaseUrl", properties.baseUrl()
);
}
}
启动后:
curl http://localhost:8080/hello
可能得到:
{
"service": "boot-foundation",
"remoteBaseUrl": "https://api.example.com"
}
这个示例中的配置值会被返回,因此只适合演示。真实服务不应把访问令牌、密码、连接字符串中的凭据或内部网络信息直接返回给客户端。
九、常见误解与边界
1. “引入 Starter 就一定有对应 Bean”
不一定。Starter 只是引入依赖,自动配置还要经过条件判断。例如:
- 缺少驱动类;
- 属性未启用;
- 用户已提供自定义 Bean;
- 应用类型不符合;
- 自动配置被显式排除。
正确诊断方式是查看依赖树、条件评估报告和最终 Bean 列表,而不是只看 pom.xml。
2. “自动配置会覆盖我的 Bean”
正常情况下,Boot 自动配置倾向于使用 @ConditionalOnMissingBean 为用户保留替换入口。但具体行为由目标自动配置的实现决定。用户 Bean 与默认 Bean 可能同时存在,进而触发歧义:
NoUniqueBeanDefinitionException
此时可以使用:
@Primary
@Bean
PaymentClient primaryPaymentClient() {
return new PaymentClient();
}
或在注入点使用:
public OrderService(@Qualifier("primaryPaymentClient")
PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
3. “Actuator 的健康状态是应用真实业务状态”
健康检查只是被实现的检查项集合。一个只检查 JVM 进程的 /health 可能返回 UP,但订单数据库已经不可写。相反,若把所有第三方系统都纳入 liveness,又可能导致无意义重启。
健康指标的含义取决于检查器、分组和部署平台如何使用它。
4. “配置文件中的值就是最终值”
配置可能被以下来源覆盖:
配置文件
< profile 配置
< 外部配置
< 系统属性
< 命令行参数
实际优先级和具体来源应以对应 Boot 版本规则为准。排查时应记录启动参数、环境变量、激活的 profile 和外部挂载文件。
5. “启动成功就代表业务可用”
启动成功只说明指定的启动阶段完成。它不保证:
- 外部服务可用;
- 数据库已有正确表结构;
- 消息消费者已正常消费;
- 业务接口参数正确;
- 所有依赖都满足业务语义。
因此,启动日志、健康端点、业务探针和真实请求验证分别证明不同层次的事实。
十、建立正确的心智模型
Spring Boot 工程可以用下面的关系理解:
Starter
└── 通过构建工具引入依赖
↓
运行时 classpath 和配置
↓
Auto-configuration 条件评估
↓
Spring IoC 注册和创建 Bean
↓
MVC/WebFlux、数据访问、客户端等能力可用
↓
Actuator 提供健康、指标和诊断出口
↓
启动与部署平台根据状态接收或停止流量
其中每一层解决不同问题:
- Spring IoC:对象由谁创建、如何注入和销毁;
- 自动配置:在什么条件下注册默认基础设施;
- Starter:工程需要引入哪些依赖;
- 配置绑定:如何把外部文本配置转换为类型化对象;
- Actuator:如何观察应用状态;
- 启动流程:如何将这些组件按生命周期组织起来。
遇到问题时,应沿这条链反向检查:
- 依赖是否进入最终运行时 classpath;
- 自动配置候选是否被发现;
- 条件是否满足;
- 配置是否成功绑定并通过校验;
- Bean 是否注册、创建和注入成功;
- Web 服务器是否启动并监听预期端口;
- Actuator 暴露的状态是否反映了实际依赖;
- 部署平台是否正确处理就绪、摘流和关闭。
理解这条链后,Spring Boot 的“约定优于配置”就不再是不可解释的隐式行为,而是由依赖、条件、配置、Bean 生命周期和运维接口共同构成的一套可诊断启动系统。
系列导航与关联阅读
- 系列入口:Java 完整学习路线:从 Java 25 语言与 JVM 到 Spring、微服务和生产交付
- 上一篇:Spring Framework 核心:IoC、Bean 生命周期、AOP、事件和事务代理
- 下一篇:Spring Web MVC 与 WebFlux:请求链、校验、异常、流式和选择边界
官方资料
本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论