Skip to content

Spring 全栈硬核教程 · 02:Spring Boot(自动配置 / Starter / 配置 / Actuator / 测试) ​

上一篇讲了 Spring 的物理定律,这一篇讲"谁替你把这些定律用起来了"。核心命题只有一个:约定优于配置的魔法,到底是怎么变出来的?


1. 拆解 @SpringBootApplication ​

java
@SpringBootConfiguration   // 本质就是 @Configuration,标记这是配置类
@EnableAutoConfiguration   // ⭐ 全部魔法的源头
@ComponentScan             // 扫描当前包及子包(这就是为什么主类要放在最外层包)
public @interface SpringBootApplication {}

踩坑提醒:@ComponentScan 默认只扫主类所在包及其子包。如果你把主类放进了 com.example.demo.app,而 Service 在 com.example.service,启动时会报"找不到 Bean"——这是新手 Top 3 问题,根因就是这个默认行为。


2. 自动配置的完整机制(源码级) ​

2.1 自动配置类是怎么被发现的 ​

Boot 版本发现机制文件路径
2.x(已过时)SpringFactoriesLoader 读 key-valueMETA-INF/spring.factories
3.x / 4.x(现行)ImportCandidates 读纯类名列表META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

新格式的内容极简,一行一个全限定类名:

com.example.myapp.autoconfigure.GreetingAutoConfiguration
com.example.myapp.autoconfigure.AnotherAutoConfiguration

❓ 为什么要换?换个文件名有什么战略意义?

这不是为了好看。老的 spring.factories 是一个大杂烩文件(里面混着十几种不同类型的扩展点),解析时要先读整个文件、按 key 分组、再筛选,启动时的 I/O 和解析开销都更大。新格式每种扩展点一个独立文件、纯列表结构,带来三个收益:

  1. 启动更快:不用解析无关内容
  2. 更适合 AOT(提前编译):编译期就能确定要加载哪些配置类,这是 GraalVM 原生镜像支持能从 Boot 3 开始突飞猛进的底层原因之一
  3. 模块化更清晰:配合 Boot 4 的代码库模块化拆分,每个模块管好自己的那份清单

2.2 条件注解:自动配置的"智能"从哪来 ​

自动配置类不是无脑生效的,靠 @Conditional 系列决定"什么情况下才装配":

注解触发条件
@ConditionalOnClassclasspath 有某个类("你引了 Redis 依赖,我才给你配 Redis")
@ConditionalOnMissingBean⭐ 容器里没有这个类型的 Bean("你自己配了,我就不插手")
@ConditionalOnBean容器里有某个 Bean
@ConditionalOnProperty某个配置项的值满足条件
@ConditionalOnWebApplication是不是 Web 应用(还细分 Servlet / Reactive)
@ConditionalOnResourceclasspath 有某个资源文件
@ConditionalOnExpressionSpEL 表达式为真

@ConditionalOnMissingBean 是整个自动配置体系的灵魂——它实现了"用户自定义永远优先于框架默认"这条铁律。你只要自己定义一个 DataSource Bean,官方的自动配置就会自动让位,全程不需要任何"关闭默认配置"的开关。

java
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean          // ← 灵魂所在
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

2.3 排查自动配置问题的黄金手段 ​

yaml
debug: true

加上这一行,启动日志会打印 CONDITIONS EVALUATION REPORT:

Positive matches:   (生效了的,附带生效原因)
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource'

Negative matches:   (⭐ 没生效的,附带没生效的具体原因,排查神器)
-----------------
   RedisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.data.redis.core.RedisOperations'

实战价值:当你遇到"我明明加了依赖,为什么这个 Bean 没有"时,别再瞎猜了,看 Negative matches 里的原因,答案通常就一行字。这个技巧能省掉的时间,一年下来是以天计的。


3. 手写一个企业级 Starter(完整可用) ​

假设我们要做一个"全公司统一的接口日志 Starter"。

3.1 项目结构 ​

my-logging-spring-boot-starter/
├── pom.xml
└── src/main/
    ├── java/com/company/logging/
    │   ├── LoggingProperties.java        # 配置属性
    │   ├── LoggingAspect.java            # 核心功能
    │   └── LoggingAutoConfiguration.java # 自动配置类
    └── resources/META-INF/spring/
        └── org.springframework.boot.autoconfigure.AutoConfiguration.imports

3.2 配置属性类(支持 IDE 提示) ​

java
@ConfigurationProperties(prefix = "company.logging")
public record LoggingProperties(
    /** 是否启用,默认 true */
    @DefaultValue("true") boolean enabled,
    /** 慢接口阈值(ms),超过则告警 */
    @DefaultValue("500") long slowThresholdMs,
    /** 需要脱敏的字段名 */
    @DefaultValue({"password", "idCard", "phone"}) List<String> sensitiveFields
) {}

加分项:引入 spring-boot-configuration-processor 依赖后,你写的 Javadoc 注释会被编译进 spring-configuration-metadata.json,使用者在 application.yml 里敲 company.logging. 时 IDE 会自动提示所有配置项和说明。这是"内部框架是否专业"的一个直观指标。

3.3 自动配置类 ​

java
@AutoConfiguration
@EnableConfigurationProperties(LoggingProperties.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnProperty(prefix = "company.logging", name = "enabled",
                       havingValue = "true", matchIfMissing = true)
public class LoggingAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public LoggingAspect loggingAspect(LoggingProperties props) {
        return new LoggingAspect(props);
    }
}

3.4 注册 ​

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

com.company.logging.LoggingAutoConfiguration

3.5 命名规范(别踩商标坑) ​

类型命名例子
官方 Starterspring-boot-starter-*spring-boot-starter-web
第三方 Starter*-spring-boot-startermybatis-plus-spring-boot-starter

官方明确要求第三方不要用 spring-boot-starter- 开头,这是命名空间约定。国内很多内部框架乱用前缀,开源时会有麻烦。

3.6 自动配置的顺序控制 ​

java
@AutoConfiguration(
    after = { DataSourceAutoConfiguration.class },    // 在数据源配置好之后
    before = { WebMvcAutoConfiguration.class }        // 在 MVC 配置之前
)
public class MyAutoConfiguration {}

当你的 Starter 依赖其他 Bean 已经就绪时,必须用 after/before 显式声明顺序,否则会遇到间歇性的"Bean 找不到"——这类问题最难排查,因为它依赖类加载顺序,本地能跑线上挂。


4. 配置体系:优先级、Profile 与配置中心 ​

4.1 外部化配置优先级(高覆盖低) ​

1.  命令行参数            --server.port=9000
2.  SPRING_APPLICATION_JSON 环境变量
3.  ServletConfig / ServletContext 参数
4.  Java 系统属性         -Dserver.port=9000
5.  操作系统环境变量       SERVER_PORT=9000
6.  随机值                ${random.uuid}
7.  jar 外部的 application-{profile}.yml   ← ⭐ 生产部署最常用
8.  jar 内部的 application-{profile}.yml
9.  jar 外部的 application.yml
10. jar 内部的 application.yml
11. @PropertySource
12. SpringApplication.setDefaultProperties

记忆口诀:命令行 > 环境变量 > 外部文件 > 内部文件 > 代码默认值。

生产最佳实践:镜像里打包开发默认配置,敏感信息(数据库密码、密钥)全部走环境变量或外挂配置文件注入。这样才能实现"一次构建,多环境部署"——同一个镜像 tag 从测试环境一路升到生产,杜绝"测试环境好好的,生产打的包不一样"这类事故。

4.2 Profile 分环境 ​

yaml
spring:
  application:
    name: order-service
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}   # 环境变量优先,本地默认 dev

---
spring.config.activate.on-profile: dev
logging.level.com.example: DEBUG

---
spring.config.activate.on-profile: prod
logging.level.com.example: WARN

进阶技巧:Profile Group。当环境维度变多(dev/test/staging/prod × 单机/集群),可以用 spring.profiles.group.prod=prod-db,prod-mq,prod-cache 把多个 profile 打包成一组激活,避免命令行里写一长串。

4.3 配置绑定与校验 ​

java
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @Email String from,
    @DurationUnit(ChronoUnit.SECONDS) Duration timeout   // 支持 "30s" "5m" 这种写法
) {}

用 Record 绑定配置的三个好处:不可变(线程安全)、无需 getter/setter 样板、字段一目了然。Boot 3+ 原生支持,Boot 4 下应该成为默认写法。

@Validated 的价值是 fail-fast:配置错了在启动时就报错退出,而不是等到运行三小时后某个冷门分支被触发时才炸。这对 K8s 环境尤其重要——启动失败会被探针发现并阻止流量进入,而运行时炸则是真实的线上故障。

4.4 本地配置 vs 配置中心 ​

维度本地 ymlNacos / Apollo
修改生效重启动态推送(配合 @RefreshScope)
多实例一致性靠人工/CI 保证中心化,天然一致
灰度/回滚无支持灰度发布、版本回滚、变更审计
适用单体、小项目微服务集群、需要动态调参(限流阈值、功能开关)

反常识提醒:配置中心不是银弹。它引入了新的单点依赖——配置中心挂了,新实例起不来。生产上必须配置本地快照兜底(Nacos 默认有),并且定期演练"配置中心不可用"的场景。见过太多团队上了配置中心却从没测过它挂掉会怎样。


5. Actuator:生产可观测性的入口 ​

5.1 基本配置 ​

yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus   # ⚠️ 绝不要写 "*"
      base-path: /internal/actuator                # 改掉默认路径,降低被扫描风险
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true      # 开启 K8s 风格的 liveness/readiness 子端点
  server:
    port: 9001             # ⭐ 管理端点单独开端口,不对外网暴露

安全红线:include: "*" 会暴露 /actuator/env(能看到所有配置,包括数据库密码)、/actuator/heapdump(能下载堆转储,里面可能有明文敏感数据)。这是真实发生过的安全事故类型,务必按需暴露 + 单独端口 + 网络层隔离三重防护。

5.2 自定义健康检查 ​

java
@Component
@RequiredArgsConstructor
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentClient client;

    @Override
    public Health health() {
        try {
            long start = System.currentTimeMillis();
            client.ping();
            long cost = System.currentTimeMillis() - start;
            return cost < 1000
                ? Health.up().withDetail("latencyMs", cost).build()
                : Health.status("DEGRADED").withDetail("latencyMs", cost).build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

5.3 liveness vs readiness(K8s 场景必须分清) ​

探针语义失败后果该检查什么
liveness(存活)进程是不是死了/卡死了重启容器只检查自身状态,绝不要检查下游依赖
readiness(就绪)能不能接流量了摘掉流量,不重启可以检查数据库、缓存等强依赖

经典生产事故:把数据库检查放进 liveness → 数据库抖动 → 所有 Pod 探针失败 → K8s 全部重启 → 重启后仍连不上数据库 → 无限重启风暴,把一次数据库小抖动放大成全站雪崩。这个坑每年都有团队踩,记住那条铁律:liveness 只看自己,readiness 才看依赖。


6. 测试体系:从单测到 Testcontainers ​

6.1 测试分层 ​

层次注解启动内容速度用途
纯单元测试@ExtendWith(MockitoExtension.class)不启容器毫秒测业务逻辑
切片测试@WebMvcTest / @DataJpaTest只启相关那一层百毫秒测 Controller 参数绑定、测 Repository SQL
集成测试@SpringBootTest整个容器秒级端到端链路验证

6.2 切片测试(被严重低估的能力) ​

java
@WebMvcTest(BookController.class)   // 只启 Web 层,不碰数据库
class BookControllerTest {

    @Autowired MockMvc mockMvc;
    @MockitoBean BookService bookService;   // Boot 3.4+ 起用 @MockitoBean(原 @MockBean 已弃用)

    @Test
    void shouldReturn422WhenTitleBlank() throws Exception {
        mockMvc.perform(post("/api/v1/books")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"title":"", "author":"刘慈欣", "price":59.9}
                """))
            .andExpect(status().isUnprocessableEntity());
    }
}

切片测试的价值:只启动你要测的那一层,比 @SpringBootTest 快一个数量级。一个有 500 个测试的项目,全用 @SpringBootTest 要跑 10 分钟,合理分层后能压到 1 分钟以内——这直接决定了团队愿不愿意写测试。

6.3 Testcontainers:告别 H2 自欺欺人 ​

java
@SpringBootTest
@Testcontainers
class BookRepositoryIT {

    @Container
    @ServiceConnection      // ⭐ Boot 3.1+ 起,自动把容器连接信息注入配置,不用再写 @DynamicPropertySource
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    @Container
    @ServiceConnection
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine").withExposedPorts(6379);

    @Test
    void shouldHandleMySqlSpecificSyntax() {
        // 跑在真实 MySQL 8.0 上,不是行为有差异的 H2
    }
}

为什么这很重要:用 H2 做集成测试是行业里最普遍的自欺欺人行为。H2 和 MySQL 在 SQL 方言、字符集、日期处理、锁行为上都有差异,测试全绿 ≠ 生产没问题。Testcontainers 的代价是慢几十秒,收益是把一类"只在生产出现"的 bug 提前到 CI 阶段。

@ServiceConnection 是 Boot 3.1 引入的重要简化——过去要手写 @DynamicPropertySource 把容器的 host/port 塞进配置,现在框架自动完成。


7. 打包与镜像优化 ​

7.1 分层 Jar:让 Docker 缓存真正生效 ​

bash
java -Djarmode=tools -jar app.jar extract --layers --launcher

Spring Boot 的可执行 jar 天然支持按变更频率分层:

dependencies/          ← 很少变(第三方库)
spring-boot-loader/    ← 几乎不变
snapshot-dependencies/ ← 偶尔变
application/           ← 每次都变(你的业务代码)
dockerfile
FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
# 按变更频率从低到高 COPY,让 Docker 层缓存最大化命中
COPY --from=build /app/extracted/dependencies/ ./
COPY --from=build /app/extracted/spring-boot-loader/ ./
COPY --from=build /app/extracted/snapshot-dependencies/ ./
COPY --from=build /app/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "app.jar"]

效果:只改业务代码时,前三层全部命中缓存,推送到镜像仓库的增量从 80MB 降到几百 KB,CI/CD 时间显著缩短。对一天部署几十次的团队,这个优化的复利很可观。

7.2 Buildpacks:不写 Dockerfile 也能出好镜像 ​

bash
mvn spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:1.0

Cloud Native Buildpacks 会自动选基础镜像、设 JVM 参数、做分层优化。适合不想维护 Dockerfile 的团队;不适合需要精细控制基础镜像(比如公司有强制的合规基础镜像)的场景。


8. 本篇自测 ​

  1. @SpringBootApplication 由哪三个注解组成?为什么主类必须放在最外层包?
  2. 自动配置的发现机制在 Boot 2 和 Boot 3/4 有什么区别?换文件格式的深层动机是什么?
  3. @ConditionalOnMissingBean 为什么被称为自动配置体系的灵魂?
  4. 外部化配置的优先级顺序是什么?"一次构建多环境部署"该怎么落地?
  5. liveness 和 readiness 探针的本质区别是什么?把数据库检查放进 liveness 会引发什么事故?
  6. 为什么说用 H2 做集成测试是自欺欺人?Testcontainers 的 @ServiceConnection 解决了什么问题?
  7. 分层 Jar 是怎么提升 CI/CD 效率的?

下一篇(03 · Spring Web):一个 HTTP 请求从网卡到你的 @RestController 方法,中间要过多少道关卡?HandlerMethodArgumentResolver 怎么让你写出优雅的 @CurrentUser?以及那个终极问题——虚拟线程时代,还有必要学 WebFlux 吗?

基于 Vite 强力驱动 | 纯静态轻量托管