Spring 全栈硬核教程 · 02:Spring Boot(自动配置 / Starter / 配置 / Actuator / 测试)
上一篇讲了 Spring 的物理定律,这一篇讲"谁替你把这些定律用起来了"。核心命题只有一个:约定优于配置的魔法,到底是怎么变出来的?
1. 拆解 @SpringBootApplication
@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-value | META-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 和解析开销都更大。新格式每种扩展点一个独立文件、纯列表结构,带来三个收益:
- 启动更快:不用解析无关内容
- 更适合 AOT(提前编译):编译期就能确定要加载哪些配置类,这是 GraalVM 原生镜像支持能从 Boot 3 开始突飞猛进的底层原因之一
- 模块化更清晰:配合 Boot 4 的代码库模块化拆分,每个模块管好自己的那份清单
2.2 条件注解:自动配置的"智能"从哪来
自动配置类不是无脑生效的,靠 @Conditional 系列决定"什么情况下才装配":
| 注解 | 触发条件 |
|---|---|
@ConditionalOnClass | classpath 有某个类("你引了 Redis 依赖,我才给你配 Redis") |
@ConditionalOnMissingBean | ⭐ 容器里没有这个类型的 Bean("你自己配了,我就不插手") |
@ConditionalOnBean | 容器里有某个 Bean |
@ConditionalOnProperty | 某个配置项的值满足条件 |
@ConditionalOnWebApplication | 是不是 Web 应用(还细分 Servlet / Reactive) |
@ConditionalOnResource | classpath 有某个资源文件 |
@ConditionalOnExpression | SpEL 表达式为真 |
@ConditionalOnMissingBean 是整个自动配置体系的灵魂——它实现了"用户自定义永远优先于框架默认"这条铁律。你只要自己定义一个 DataSource Bean,官方的自动配置就会自动让位,全程不需要任何"关闭默认配置"的开关。
@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 排查自动配置问题的黄金手段
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.imports3.2 配置属性类(支持 IDE 提示)
@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 自动配置类
@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.LoggingAutoConfiguration3.5 命名规范(别踩商标坑)
| 类型 | 命名 | 例子 |
|---|---|---|
| 官方 Starter | spring-boot-starter-* | spring-boot-starter-web |
| 第三方 Starter | *-spring-boot-starter | mybatis-plus-spring-boot-starter |
官方明确要求第三方不要用 spring-boot-starter- 开头,这是命名空间约定。国内很多内部框架乱用前缀,开源时会有麻烦。
3.6 自动配置的顺序控制
@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 分环境
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 配置绑定与校验
@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 配置中心
| 维度 | 本地 yml | Nacos / Apollo |
|---|---|---|
| 修改生效 | 重启 | 动态推送(配合 @RefreshScope) |
| 多实例一致性 | 靠人工/CI 保证 | 中心化,天然一致 |
| 灰度/回滚 | 无 | 支持灰度发布、版本回滚、变更审计 |
| 适用 | 单体、小项目 | 微服务集群、需要动态调参(限流阈值、功能开关) |
反常识提醒:配置中心不是银弹。它引入了新的单点依赖——配置中心挂了,新实例起不来。生产上必须配置本地快照兜底(Nacos 默认有),并且定期演练"配置中心不可用"的场景。见过太多团队上了配置中心却从没测过它挂掉会怎样。
5. Actuator:生产可观测性的入口
5.1 基本配置
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 自定义健康检查
@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 切片测试(被严重低估的能力)
@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 自欺欺人
@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 缓存真正生效
java -Djarmode=tools -jar app.jar extract --layers --launcherSpring Boot 的可执行 jar 天然支持按变更频率分层:
dependencies/ ← 很少变(第三方库)
spring-boot-loader/ ← 几乎不变
snapshot-dependencies/ ← 偶尔变
application/ ← 每次都变(你的业务代码)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 也能出好镜像
mvn spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:1.0Cloud Native Buildpacks 会自动选基础镜像、设 JVM 参数、做分层优化。适合不想维护 Dockerfile 的团队;不适合需要精细控制基础镜像(比如公司有强制的合规基础镜像)的场景。
8. 本篇自测
@SpringBootApplication由哪三个注解组成?为什么主类必须放在最外层包?- 自动配置的发现机制在 Boot 2 和 Boot 3/4 有什么区别?换文件格式的深层动机是什么?
@ConditionalOnMissingBean为什么被称为自动配置体系的灵魂?- 外部化配置的优先级顺序是什么?"一次构建多环境部署"该怎么落地?
- liveness 和 readiness 探针的本质区别是什么?把数据库检查放进 liveness 会引发什么事故?
- 为什么说用 H2 做集成测试是自欺欺人?Testcontainers 的
@ServiceConnection解决了什么问题? - 分层 Jar 是怎么提升 CI/CD 效率的?
下一篇(03 · Spring Web):一个 HTTP 请求从网卡到你的
@RestController方法,中间要过多少道关卡?HandlerMethodArgumentResolver怎么让你写出优雅的@CurrentUser?以及那个终极问题——虚拟线程时代,还有必要学 WebFlux 吗?