Java 基础体系 · 第 25/100 篇。示例统一以 Java 25 LTS 为语言和 JVM 基线;框架示例使用与其兼容的现代稳定版本。

Spring Boot 工程基础:自动配置、Starter、配置绑定、Actuator 和启动

Spring Boot 不是另一个取代 Spring Framework 的容器。它建立在 Spring Framework 的 IoC 容器、Bean 生命周期、事件机制、AOP、事务代理和 Web 能力之上,主要解决三个工程问题:

  1. 如何根据 classpath 和配置自动装配一组合理的 Bean;
  2. 如何以可复用的依赖组合引入技术能力;
  3. 如何让应用具备可配置、可观测、可启动和可部署的工程结构。

本文以 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;
    }
}

容器启动时大致经历以下过程:

  1. 扫描组件或读取配置类;
  2. 注册 BeanDefinition;
  3. 解析构造器依赖;
  4. 创建 PaymentClient
  5. 创建 OrderService 并注入依赖;
  6. 执行 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());
    }
}

这个配置类表达了两个条件:

  1. classpath 中存在 RedisClient
  2. 容器中没有用户自己提供的 RedisClient Bean。

只有两个条件都满足时,默认 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,也可以使用带有相应元数据的配置类。核心并不在注解名字,而在于:

  1. 候选配置被发现;
  2. 条件被评估;
  3. 满足条件则导入;
  4. 配置类中的 Bean 定义进入 IoC 容器;
  5. 用户定义的 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

诊断时应先问三个问题:

  1. 目标类是否真的在运行时 classpath 中;
  2. 属性名和值是否符合条件;
  3. 是否已经存在一个使 @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,强行升级后会在启动或运行时出现 NoSuchMethodErrorClassNotFoundException 等二进制兼容问题。

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 中有三个容易混淆的概念:

  1. 端点是否启用:端点 Bean 是否创建;
  2. 端点是否暴露:是否通过 HTTP、JMX 等方式提供;
  3. 端点是否允许访问:是否通过认证和授权。

例如:

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 类型会有实现细节差异,但重要的因果关系是:

  1. 配置先进入 Environment;
  2. 自动配置和组件扫描形成 Bean 定义;
  3. refresh 期间创建 Bean;
  4. Web 应用创建并启动嵌入式服务器;
  5. 应用进入可服务状态;
  6. 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 容器会进入关闭流程:

  1. 停止接收新的请求,具体行为由 Web 服务器和部署方式共同决定;
  2. 执行生命周期组件的停止逻辑;
  3. 调用 Bean 的销毁回调;
  4. 关闭线程池、连接池等资源。

例如:

@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

修复路径是:

  1. 确认占用者是否为旧实例;
  2. 若旧实例应退出,先正常停止;
  3. 若端口确实需要共存,修改 server.port
  4. 重新启动并用 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:如何观察应用状态;
  • 启动流程:如何将这些组件按生命周期组织起来。

遇到问题时,应沿这条链反向检查:

  1. 依赖是否进入最终运行时 classpath;
  2. 自动配置候选是否被发现;
  3. 条件是否满足;
  4. 配置是否成功绑定并通过校验;
  5. Bean 是否注册、创建和注入成功;
  6. Web 服务器是否启动并监听预期端口;
  7. Actuator 暴露的状态是否反映了实际依赖;
  8. 部署平台是否正确处理就绪、摘流和关闭。

理解这条链后,Spring Boot 的“约定优于配置”就不再是不可解释的隐式行为,而是由依赖、条件、配置、Bean 生命周期和运维接口共同构成的一套可诊断启动系统。


系列导航与关联阅读

官方资料

本文依据 Java、Spring 与相关项目官方文档重新梳理;正文、示例与生产清单由 WR BLOG 编写。