第 06 篇 · Spring Boot 与现代框架
本篇把 Boot 看成「约定装配 + 生产就绪」:自动配置/Starter → 安全与校验 → AOT/Native → Actuator/配置/停机与云原生测试闭环。
骨架见 M12.06。本篇只补领域细枝,框在 核心方法论 · 开篇。
Boot 主线是「约定装配与可覆盖 → 安全过滤器链与校验 → 启动编排与外部化配置 → AOT/Native 与可观测/优雅停机 → 测试与替代栈权衡」,目标是可运维默认值。
- 自动配置 SPI 与 Starter:EnableAutoConfiguration + Conditional 条件过滤;Starter 用「约定+默认+可覆盖」封装可复用产物。
- 启动编排与配置优先级:SpringApplication 事件/Runner/嵌入式容器叠加 refresh;多 PropertySource 叠加冲突用 env 端点定位终值。
- Security 与 OAuth2:FilterChainProxy 双阶段认证授权;Security 6 链式 DSL、JWT/Session 选型与微服务令牌传递。
- 校验、异常与 Session:Bean Validation 声明式校验;@ControllerAdvice 统一错误出口;Spring Session 外置会话存储。
- AOT/Native 与可观测停机:构建期冻结反射与条件;Actuator 端点安全暴露;优雅停机与 K8s 探针/摘流时序对齐。
- 测试策略与栈选型:Testcontainers/契约/ArchUnit 防漂移;Quarkus/Micronaut 与 Boot 在启动内存与生态间权衡。
- 统一 Starter / 基线 BOM;安全补丁与依赖抬升由平台节奏驱动,业务少私自锁版本,例外走审批。
- 自动配置「未生效/误生效」排障先查条件匹配与排除;自定义 Starter 必须可覆盖默认 Bean 并有示例工程。
- Security 过滤器链与端点暴露进上线门禁:默认拒绝、最小开放、CSRF/CORS/鉴权路径有集成测。
- 配置优先级写成团队约定表;同名键冲突用 Actuator/env 查来源,禁止靠背口诀上线。
- 全局异常与校验错误响应体标准化:状态码映射、脱敏、埋点一致;禁止堆栈回传客户端。
- AOT/Native 构建与功能回归解耦验收:反射/资源/代理到达清单维护,失败可回退 JVM 模式。
- 滚动发布验收:readiness 先摘流、排水超时、连接拒绝策略与 K8s preStop 对齐,杜绝更新窗口 5xx。
- 质量门禁含 Testcontainers 真依赖烟测、消费者契约与 ArchUnit 分层规则;纯 mock 集成测不得单独作为放行依据。
- 约定可覆盖:默认要快,覆盖路径必须文档化、可测、可审计。
- 条件即真相:装配问题先打印条件评估,而不是先删依赖碰运气。
- 安全尽量早:过滤器链在业务前;端点与鉴权是生产默认题,不是加分项。
- 配置有终值:多源叠加下只认运行时终值与来源,不认「我改了 yml」。
- 编译范式要匹配场景:Native 换启动与内存,付出构建与闭环世界约束;按负载选型。
- 生命周期对齐编排:应用停机与 K8s 摘流时序不一致,就会在「正常发布」里制造故障。
- 测试防三类腐化:环境漂移、接口背刺、结构腐化分别用容器、契约、架构规则对付。
这道题考察对 Spring Boot "约定优于配置"核心机制的底层理解:@EnableAutoConfiguration 如何通过类路径扫描 + 条件过滤,把第三方 jar 里的配置类自动注入容器。本质是一套基于 SPI(Service Provider Interface)的自动装配机制——框架定义加载契约,各 Starter 按契约声明自己的配置类,Boot 在启动期读取声明、用 @Conditional 系列注解做条件裁剪,最终只装配"环境匹配"的 Bean。理解它就能回答"为什么引入 starter-data-redis 就能用 RedisTemplate""为什么自动配置类不冲突""如何禁用某个自动配置"等一切自动配置问题。
Spring Boot 1.x(2014):确立 spring.factories 作为自动配置的 SPI 声明文件,位于 META-INF/spring.factories,以 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key 列出全限定类名。加载器 SpringFactoriesLoader 用 Properties 解析,全量加载、无去重。
Spring Boot 2.x(2018):沿用 spring.factories,但引入 @AutoConfigureBefore/After/Order 控制配置类顺序;@ConditionalOnBean
等条件注解体系成熟。问题浮现:spring.factories 被滥用——除自动配置外还塞了监听器、初始化器、环境后置处理器等十几种 key,启动期要加载上百个配置类做条件评估,拖慢启动。
Spring Boot 3.0(2022):关键演进——自动配置从 spring.factories 迁移到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,每行一个类名。spring.factories 中 EnableAutoConfiguration key 不再生效(Boot 3.0 起写了也不会被加载)。新机制用 ImportCandidates 读取,去重 + 排序更高效,且为 AOT 处理铺路——AOT 阶段需要确定的、可静态分析的配置清单,spring.factories 的 Properties 格式不利于工具分析。
趋势:自动配置正与 AOT/Native Image 深度融合,条件评估在构建期冻结;未来可能进一步向"声明式配置 + 构建期生成"演进,减少运行期反射与条件判断。
触发入口:@SpringBootApplication = @EnableAutoConfiguration + @ComponentScan + @SpringBootConfiguration。@EnableAutoConfiguration 通过
@Import(AutoConfigurationImportSelector.class)
导入选择器,这是自动装配的真正引擎。
2.x: SpringFactoriesLoader 读 spring.factories
3.x: ImportCandidates 读 AutoConfiguration.imports"] F --> G["2、去重 deduplication"] G --> H["3、排序 AutoConfigureOrder/Before/After"] H --> I["4、过滤 @Conditional 条件评估"] I --> J{"条件满足?"} J -- 是 --> K["注册该配置类为 BeanDefinition"] J -- 否 --> L["排除该配置类"] K --> M["后续由 ConfigurationClassPostProcessor
解析 @Bean 方法注册 Bean"] style F fill:#dbeafe,stroke:#2563eb style I fill:#fef3c7,stroke:#d97706
2.x 的 spring.factories 格式(Properties 风格,多类名逗号分隔;已废弃:Spring Boot 3.0 起该 key 已移除不再被加载(写了也不会生效),替代:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports):
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.RedisAutoConfiguration,\
com.example.MongoAutoConfiguration
org.springframework.context.ApplicationListener=\
com.example.MyListener
3.x 的 AutoConfiguration.imports 格式(每行一个类名,纯文本):
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.RedisAutoConfiguration
com.example.MongoAutoConfiguration
条件过滤:@Conditional 系列注解。这是自动配置"智能"的关键——每个配置类/Bean 方法标注条件,ConditionEvaluator 在注册前评估,不满足则跳过:
| 条件注解 | 判定逻辑 | 典型场景 |
|---|---|---|
@ConditionalOnClass |
类路径存在指定类 | 有 redis-client 才装配 RedisTemplate |
@ConditionalOnMissingBean |
容器中无指定 Bean | 用户未自定义则用默认实现 |
@ConditionalOnBean |
容器中存在指定 Bean | 有 DataSource 才装配 JdbcTemplate |
@ConditionalOnProperty |
配置属性匹配 | spring.cache.type=redis 才启用 |
@ConditionalOnWebApplication |
是否 Web 环境 | 仅 Web 应用装配 DispatcherServlet |
为何 @ConditionalOnMissingBean 能实现"用户配置优先":Spring 加载顺序保证用户 @Configuration 先于自动配置类被解析(自动配置类通过 @AutoConfigureAfter 排在后)。因此用户定义的 Bean 先注册,自动配置类的
@ConditionalOnMissingBean
检测到已存在则跳过默认实现。这是"约定优于配置,但配置可覆盖约定"的底层保证。
调试自动配置:启动时加 --debug 或 logging.level.org.springframework.boot.autoconfigure=debug,会打印 Conditions Evaluation Report,列出哪些配置类匹配、哪些被排除及原因,是排查"为什么没自动装配"的第一手段。
排除特定自动配置:
// 方式1: 启动类排除
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App { }
// 方式2: 配置文件排除
# application.yml
spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration
自定义自动配置类务必遵循官方规范:① 放在独立 starter 模块,不暴露给业务;② 用
@AutoConfiguration(3.x)标注而非 @Configuration,表明是自动配置类;③ 用 @ConditionalOnClass 守卫,避免缺依赖时报错;④ 默认 Bean 一律加 @ConditionalOnMissingBean,给用户覆盖空间;⑤ 用 @ConfigurationProperties 而非 @Value 暴露可配置项。
追问 1:3.x 为何要废弃 spring.factories 改用 AutoConfiguration.imports?
三个原因:① 性能——spring.factories 是 Properties 格式,一个 key 可挂多个逗号分隔类名,加载需解析整个文件;imports 文件纯文本每行一类,ImportCandidates 读取更轻量;② 职责单一——spring.factories 混装了十几种 SPI(监听器、初始化器、环境后置处理器等),自动配置与它们耦合在同一文件,难以独立管理与工具化;③ AOT 友好——AOT 处理器需要"确定且可静态分析"的配置清单,imports 文件结构清晰,便于构建期生成 AOT 元数据。注意:仅 EnableAutoConfiguration 这个 key 迁移,其他 SPI(如 EnvironmentPostProcessor)仍在 spring.factories。
追问 2:自动配置类的加载顺序如何保证?@ConditionalOnBean 会不会因顺序失败?
顺序通过三层保证:① @AutoConfigureOrder 数值排序;② @AutoConfigureBefore/After 显式声明前后依赖;③ AutoConfigurationSorter 做拓扑排序。但 @ConditionalOnBean 确实有顺序陷阱——条件评估在配置类注册阶段,若 A 配置类依赖 B 配置类的 Bean,但 A 排在 B 前面,@ConditionalOnBean 会误判为不存在。解决:用 @AutoConfigureAfter(B.class) 强制顺序,或改用 @ConditionalOnClass(类路径判定不依赖 Bean 注册顺序)。这也是官方推荐用 @ConditionalOnClass/@ConditionalOnMissingBean,慎用 @ConditionalOnBean
的原因。
- 误以为自动配置靠类路径扫描:扫描(
@ComponentScan)只扫用户包,自动配置靠 SPI 文件声明 +ImportSelector加载,两者机制不同。 - 3.x 仍往 spring.factories 写自动配置:3.0 起该 key 已移除不再被加载(写了也不会生效),必须迁移到 imports 文件。
- @ConditionalOnBean 顺序坑:依赖另一配置类的 Bean 时未加
@AutoConfigureAfter,导致条件误判、Bean 不装配。 - 自动配置类写在主应用包内:会被
@ComponentScan重复扫描,绕过条件评估,应放独立 starter 包。 - 排除自动配置后 Bean 仍被注入:排除的是配置类,若该 Bean 被其他配置类
@Import仍会生效,需排查全部来源。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察对 Java 编译范式从 JIT(即时编译)→ AOT(提前编译)→ GraalVM Native Image(原生镜像)演进的本质理解。AOT 不是"换个编译器",而是把运行期才做的 Bean 定义解析、条件评估、反射调用,前移到构建期冻结为静态代码;Native Image 更进一步,用闭世界分析把整个应用编译成单一原生可执行文件。这背后是 Java 从"一次编写到处运行"向"一次构建到处运行"的范式转移——牺牲部分动态性换取极致启动速度与内存占用,适应云原生/Serverless 场景。
JIT 时代(HotSpot,1990s 至今):字节码运行期解释执行 + JIT(C1/C2 编译器)热点编译。优势是动态性极强——反射、动态代理、运行期类加载、字节码生成(CGLIB/ByteBuddy)都支持;劣势是冷启动慢(要加载类、解析注解、初始化容器)、内存占用高(JVM 自身开销)。传统单体长进程下 JIT 优势明显,但云原生短生命周期容器里成了负担。
Spring Native 实验期(2020-2022):Spring 团队推出 spring-native 实验项目,用 @NativeHint 手动为 GraalVM 提供反射/代理元数据。痛点明显:开发者要手写大量 hint,框架的动态性(Spring AOP、Jackson 序列化)与 Native Image 的闭世界假设冲突严重,迁移成本高。
Spring Boot 3.0(2022)原生支持 AOT + Native Image:里程碑演进——AOT 处理器(org.springframework.boot.aot.AotProcessor)在构建期自动分析 BeanFactory,为每个 Bean 生成 BeanFactoryInitializationAotContribution(注册代码)和 BeanRegistrationAotContribution(实例化代码),用生成的静态代码替代运行期反射。同时自动生成 GraalVM reachability metadata(反射/资源/代理配置),无需手写 hint。这是 Spring 框架为云原生做的一次底层重构。
趋势:CRaC(Coordinated Restore at Checkpoint)成为 JIT 与 Native 之外的第三条路——JVM 快照恢复,启动毫秒级且保留 JIT 优化;Project Leyden 推动 Java 平台本身支持 AOT;GraalVM 持续降低构建时间与闭世界限制。未来三者将共存互补。
AOT 处理:构建期冻结 Spring 动态性。Spring 的核心动态性来自三点:① 运行期反射扫描 @Configuration/@Bean 注册 BeanDefinition;② @Conditional 运行期评估;③ 反射实例化 Bean、CGLIB 生成代理。AOT 把这三点前移:
但不实例化 Bean"] B2["2、org.springframework.boot.aot.AotProcessor 分析 BeanFactory"] B3["3、评估 @Conditional 并冻结结果"] B4["4、为每个 Bean 生成实例化代码
(替代运行期反射)"] B5["5、生成 GraalVM 元数据
(反射/资源/代理 hint)"] B1 --> B2 --> B3 --> B4 --> B5 end subgraph Run [运行期] R1["JVM 模式: 直接用 AOT 生成的
BeanDefinition 注册代码,跳过反射扫描"] R2["Native 模式: SubstrateVM 闭世界分析
+ AOT 代码 → 原生可执行文件"] end B5 --> R1 B5 --> R2 style B3 fill:#fef3c7,stroke:#d97706 style B4 fill:#dbeafe,stroke:#2563eb style B5 fill:#dcfce7,stroke:#059669 style R2 fill:#f3e8ff,stroke:#7c3aed
三大范式对比:
| 维度 | JIT(传统) | AOT(JVM 模式) | Native Image |
|---|---|---|---|
| 启动时间 | 秒级(2-10s) | 亚秒级(数百 ms) | 毫秒级(10-100ms) |
| 内存占用 | 高(200MB+) | 中(100MB) | 极低(20-50MB) |
| 峰值吞吐 | 最高(JIT 热点优化) | 高(仍运行在 JVM) | 较低(无 JIT,部分优化 PGO 可补) |
| 构建时间 | 快(秒级) | 较快(+10-20s) | 极慢(分钟级,闭世界分析) |
| 动态性 | 完全支持 | 受限(部分反射需 hint) | 严格受限(闭世界) |
| 适用场景 | 长生命周期服务 | 兼顾启动与吞吐 | Serverless/Function/CLI |
GraalVM Native Image 的核心:闭世界分析(Closed-World Analysis)。SubstrateVM 在构建期从 main 方法出发,静态分析所有可达代码,假定运行期不会出现分析期不可达的类/方法/反射目标。因此:
- 反射必须提前声明:哪些类会被反射访问、哪些字段/方法会被
setAccessible,都要在 reachability metadata 中声明,否则运行期抛ClassNotFoundException。Spring AOT 自动生成大部分,但第三方库自用的反射需手动@RegisterReflection或reflect-config.json。 - 动态代理受限:CGLIB/ByteBuddy 运行期生成字节码在 Native 下不可用,必须用 JDK 动态代理(接口)且代理接口在构建期已知。Spring AOT 会把 CGLIB 代理替换为构建期生成的代理类。
- 类加载被移除:所有类在构建期已编入镜像,
Class.forName动态加载、URLClassLoader等失效。
启用 Native Image 构建(Spring Boot 3.x + GraalVM):
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
# 构建
mvn -Pnative native:compile # 生成 target/app 可执行文件
mvn -Pnative native:test # 在 Native Image 下运行测试
选型决策:Native Image 不是银弹,按场景选:
- 适合 Native:Serverless Function(冷启动敏感)、CLI 工具、嵌入式设备、常驻但低 QPS 的边车服务。启动从 8s 降到 50ms、内存从 300MB 降到 40MB 是巨大收益。
- 不适合 Native:高吞吐长生命周期服务(无 JIT 峰值吞吐降 20-40%)、重度依赖运行期字节码增强(MyBatis 动态代理、某些 RPC 框架)、需频繁热部署的场景。这类用 AOT(JVM 模式) 或传统 JIT 更优。
Native 迁移遵循"三步法":① 先升级到 Boot 3.x + Java 17,跑通 JVM 模式;② 加 AOT 处理(process-aot
goal),在 JVM 下用 AOT 代码启动验证无反射错误;③ 再加 Native 编译,逐步补齐第三方库的 reachability metadata。务必开启
--trace-class-initialization 排查构建期类初始化问题。
追问 1:AOT 生成的代码具体替换了什么?为何能加速启动?
AOT 为每个 Bean 生成两类代码:① BeanRegistration——用直接 new 调用替代 BeanDefinition 的反射实例化(clazz.getDeclaredConstructor().newInstance() → new FooBean());② BeanFactoryInitialization——把 @Bean 方法的调用、属性注入、@PostConstruct 触发都生成为静态方法链。加速根源:跳过运行期注解扫描、跳过 ConditionEvaluator 重新评估、跳过反射实例化。AOT(JVM) 模式启动约快 2-3 倍,Native 模式因无 JVM 额外快 10-50 倍。
追问 2:Native Image 下 CGLIB 代理为何失效?Spring 如何解决?
CGLIB 在运行期用 ASM 动态生成子类字节码并 defineClass,Native Image 的闭世界假设不允许运行期生成新类,故失效。Spring AOT 的解决方案:构建期预生成代理类——AOT 处理器检测到需要 CGLIB 代理的 Bean,提前用 ByteBuddy 生成代理类源码并编译,运行期直接加载预生成类。但限制是:代理目标必须在构建期确定,运行期动态切换代理目标(如 proxyTargetClass 运行期配置)不被支持。这也是 Native 模式下部分 @Configuration 行为略有差异的原因。
追问 3:CRaC 与 Native Image 的本质区别?
CRaC 是"快照恢复"——在 JVM 已预热、JIT 已编译热点后做内存快照,恢复时直接还原堆与代码缓存,毫秒启动且保留 JIT 优化(峰值吞吐不降)。Native 是"重新编译"——把字节码编译为原生机器码,无 JVM、无 JIT、吞吐受限。CRaC 适合"需快速启动但也要峰值吞吐"的服务,Native 适合"启动与内存极致、可牺牲峰值"的 Function。两者是互补而非替代。
- 误以为 AOT = Native Image:AOT 是构建期代码生成,可在 JVM 模式独立使用;Native Image 必须依赖 AOT 但还包含 GraalVM 闭世界编译。两者可拆分。
- 第三方库反射未声明:如 Jackson 序列化第三方 DTO、MyBatis Mapper 代理,在 Native 下抛
ClassNotFoundException,需补 reachability metadata。 - 构建期 vs 运行期条件差异:AOT 在构建期评估
@Conditional,若运行期环境与构建期不同(如不同 profile),条件结果已冻结无法改变,需用@ConditionalOnProperty的运行期属性。 - 峰值吞吐下降被忽视:Native Image 无 JIT,CPU 密集型服务吞吐可能降 30%+,上线前必须压测而非只看启动速度。
- 构建时间暴涨:Native 编译数分钟,CI 流水线需独立 Native 构建任务,不能与普通构建混用。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察把"自动配置 SPI + 条件装配 + 外部化配置"组合成一个可复用产物的工程能力。Starter 的本质是"约定 + 默认 + 可覆盖"三原则的封装:约定(引入即用)、默认(零配置可用)、可覆盖(用户可定制)。它不是简单写个配置类,而是要回答四个设计问题:① 模块如何拆分(autoconfigure 与 starter 分离);② 配置项如何暴露与校验;③ 默认 Bean 如何让用户覆盖;④ 如何避免与官方 Starter 冲突。考察的是框架级组件设计思维,而非 API 使用。
早期(Boot 1.x):Starter 与 autoconfigure 耦合在一个 jar,配置类直接用 @Configuration,声明写在 spring.factories(已废弃:Spring Boot 3.0 起标记废弃,替代:AutoConfiguration.imports)。问题:用户只要功能不要某默认 Bean 时,难以剥离。
Boot 2.x 规范成熟:官方确立"双模块分离"——xxx-spring-boot-autoconfigure(含配置类与条件逻辑)+ xxx-spring-boot-starter(空 jar,只依赖 autoconfigure + 必需第三方库)。引入 @ConfigurationProperties + @Validated + Bean Validation(JSR 303/380)做配置校验,@AutoConfigureBefore/After
控制顺序。
Boot 2.7(2022):① 自动配置类用 @AutoConfiguration 替代 @Configuration(语义更明确,且支持 before/after/beforeName/afterName 属性);② 声明迁移到 AutoConfiguration.imports;③ 必须 AOT 友好——避免运行期反射扫描、动态类加载,保证 Starter 能用于 Native Image。
趋势:Starter 设计向"声明式 + 构建期可分析"演进,第三方 Starter 越来越注重提供 Native Image metadata(reflect-config.json、resource-config.json),降低用户迁移成本。
官方 Starter 的双模块拆分原则。以 mylib-spring-boot 为例:
mylib-spring-boot-autoconfigure
+ mylib-core (第三方库)"] end subgraph Auto ["mylib-spring-boot-autoconfigure (核心)"] A1["MyLibAutoConfiguration
@AutoConfiguration + @ConditionalOnClass"] A2["MyLibProperties
@ConfigurationProperties(prefix=mylib)"] A3["默认 Bean: MyLibClient
@ConditionalOnMissingBean + @Bean"] A4["META-INF/spring/...AutoConfiguration.imports"] A1 --> A2 A1 --> A3 A4 -.-> A1 end Starter --> Auto style A3 fill:#dbeafe,stroke:#2563eb style A4 fill:#dcfce7,stroke:#059669
为何要拆两个模块:autoconfigure 模块可独立用于不想引入第三方库依赖的场景(如只想要配置类逻辑),starter 模块是"开箱即用"的依赖聚合点。官方所有 starter(如 spring-boot-starter-data-redis)都遵循此模式,第三方应保持一致以降低用户认知成本。
配置属性绑定:@ConfigurationProperties 而非 @Value。@Value 是松散的属性占位符,@ConfigurationProperties
提供结构化、可校验、可 IDE 提示的属性绑定:
// 1. 配置属性类
@ConfigurationProperties(prefix = "mylib")
public class MyLibProperties {
private String endpoint;
private int timeout = 3000; // 默认值
private Retry retry = new Retry(); // 嵌套配置
// Lombok @Data 生成 getter/setter(项目规范)
}
// 2. 自动配置类(3.x 用 @AutoConfiguration)
@AutoConfiguration
@ConditionalOnClass(MyLibClient.class)
@EnableConfigurationProperties(MyLibProperties.class)
public class MyLibAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "mylib", name = "enabled", havingValue = "true", matchIfMissing = true)
public MyLibClient myLibClient(MyLibProperties props) {
return new MyLibClient(props.getEndpoint(), props.getTimeout());
}
}
3.x 的声明文件(必须,否则不会被加载):
# mylib-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.mylib.MyLibAutoConfiguration
在某平台项目中,把统一响应体
ApiResponse<T>、全局异常处理、统一日志 TraceId 透传等公共能力沉淀为 common-spring-boot-starter,各业务服务只引入一个依赖即获得全部能力,避免重复编码。
与官方 Starter 差异的关键点:
| 关注点 | 官方 Starter 做法 | 自定义 Starter 应遵循 |
|---|---|---|
| 命名 | spring-boot-starter-xxx |
xxx-spring-boot-starter(第三方命名规范,避免与官方混淆) |
| 默认配置 | 零配置可用 | 所有属性提供合理默认值,matchIfMissing=true |
| 可覆盖性 | @ConditionalOnMissingBean |
所有默认 Bean 必须加,给用户覆盖入口 |
| 配置提示 | 提供 additional-spring-configuration-metadata.json |
应提供,IDE 才有自动补全与文档 |
| AOT 友好 | 提供 Native metadata | 避免运行期反射,提供 reachability hint |
五个要点:① 双模块拆分(autoconfigure + starter);② 所有默认 Bean 加 @ConditionalOnMissingBean,让用户能替换;③ 用 @ConfigurationProperties 暴露配置 + 配置元数据 JSON,IDE 友好;④ 用 @ConditionalOnClass 守卫,缺依赖时不报错只跳过;⑤ 提供 AutoConfiguration.imports 声明,3.x 不能漏。
追问 1:为何官方 starter 命名是 spring-boot-starter-xxx,而第三方是 xxx-spring-boot-starter?
这是官方命名保留约定:spring-boot-starter-*
前缀保留给官方 starters,避免用户误以为是官方组件而向 Spring 提 issue。第三方必须用
*-spring-boot-starter
后缀。违反此约定虽不影响功能,但会在生态中造成混淆,且部分 IDE 插件会据此判断是否为官方组件提供特殊提示。
追问 2:Starter 如何支持配置热更新与动态生效?
普通 @ConfigurationProperties Bean 是单例,属性在启动时绑定后不会自动刷新。要支持热更新需配合 @RefreshScope(Spring Cloud)或 ConfigurationPropertiesRebinder(Spring Cloud Context),在配置变更时销毁并重建 Bean。但要注意:热更新只对 @ConfigurationProperties Bean 生效,对 @Bean 方法直接读属性构造的对象(如 DataSource)无效,这类需监听 EnvironmentChangeEvent 手动重建。设计 Starter 时应明确文档标注哪些属性支持热更新。
- 漏写 AutoConfiguration.imports:3.x 下只写
@AutoConfiguration但未声明,配置类根本不会被加载,且无任何报错。 - 默认 Bean 未加 @ConditionalOnMissingBean:用户自定义的同类型 Bean 与默认 Bean 冲突,或用户想覆盖时无法覆盖。
- 缺 @ConditionalOnClass 守卫:Starter 被引入但第三方库未引入时,类加载直接报
NoClassDefFoundError而非优雅跳过。 - 用 @Value 而非 @ConfigurationProperties:失去结构化绑定、校验、元数据提示,且 Native 下
@Value的 SpEL 解析需额外 hint。 - starter 模块写了业务代码:starter 应为空 jar 只做依赖聚合,逻辑必须在 autoconfigure 模块,否则违反官方规范。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察对 Spring Security "过滤器链 + Authentication/Authorization 双阶段"架构的整体把握。Spring Security 的本质是一个被 FilterChainProxy 代理的过滤器链,它通过 DelegatingFilterProxy 桥接 Servlet 容器与 Spring 容器,把安全逻辑插入到请求处理的最早阶段。理解它要抓住三条主线:① 请求如何进入过滤器链(FilterChainProxy → SecurityFilterChain 匹配);② 认证流程(AuthenticationManager + AuthenticationProvider + UserDetailsService);③ 授权流程(FilterSecurityInterceptor / AuthorizationFilter + AccessDecisionManager)。架构师还需掌握从 XML 到 DSL 到 Lambda DSL 的演进与 JWT/OAuth2 集成。
Spring Security 3.x(XML 时代):安全配置全靠 <http>、<authentication-manager>
等 XML 标签,配置冗长且不类型安全。过滤器链由 XML 解析生成,开发者难以直观看到链路。
Spring Security 4.x-5.x(DSL 时代):引入 @EnableWebSecurity + WebSecurityConfigurerAdapter(已废弃:Spring Security 6.x 已删除该类,替代:@Bean SecurityFilterChain 链式配置),用 configure(HttpSecurity) 方法链式配置:http.authorizeRequests().antMatchers("/admin").hasRole("ADMIN")(antMatchers 已废弃,替代:requestMatchers)。配置可读性大幅提升,但仍是匿名内部类 + 方法链风格,且 WebSecurityConfigurerAdapter 是抽象类需继承,扩展受限。
Spring Security 5.2+(Lambda DSL):DSL 全面支持 Lambda,http.authorizeRequests(req -> req.antMatchers("/admin").hasRole("ADMIN")),更简洁。同时 antMatchers 标记废弃,逐步用 requestMatchers 替代。
Spring Security 6.x / Boot 3.x(2022,无 Adapter 时代):关键演进——废弃 WebSecurityConfigurerAdapter,改为 @Bean 声明 SecurityFilterChain:http.securityMatcher("/api/**").authorizeHttpRequests(auth -> auth.anyRequest().authenticated())。原因:① 函数式配置更利于多链组合(一个应用可有多条 SecurityFilterChain);② 去继承更符合组合优于继承原则;③ authorizeRequests 也被 authorizeHttpRequests 取代(新授权管理器 AuthorizationManager 替代 AccessDecisionManager,更灵活)。
趋势:向 One-Time Token 登录、Passkey/WebAuthn、OAuth 2.1 标准化靠拢;与 Spring Authorization Server 深度整合;DSL 继续简化。
过滤器链的入口:DelegatingFilterProxy → FilterChainProxy。Servlet 容器不认识 Spring Bean,DelegatingFilterProxy 是个桥接 Filter,它通过 findWebApplicationContext() 拿到 Spring 容器,再委托给名为 springSecurityFilterChain 的 Bean(即 FilterChainProxy)。FilterChainProxy 内部持有多个 SecurityFilterChain,按 matches(request) 选择第一条匹配的链执行。
(桥接 Servlet 与 Spring)"] C --> D["FilterChainProxy
(名为 springSecurityFilterChain)"] D --> E{"遍历 SecurityFilterChain
matches(request)?"} E -- 匹配 --> F["执行该链的过滤器序列"] E -- 不匹配 --> G["尝试下一条链"] F --> F1["SecurityContextHolderFilter
加载/清理 SecurityContext"] F1 --> F2["UsernamePasswordAuthenticationFilter
处理表单登录(POST /login)"] F2 --> F3["BasicAuthenticationFilter
处理 HTTP Basic"] F3 --> F4["...其他过滤器(CSRF/CORS/Logout)"] F4 --> F5["AuthorizationFilter(6.x)
授权检查"] F5 --> F6["剩余 Servlet 过滤器 + Controller"] style D fill:#dbeafe,stroke:#2563eb style F2 fill:#fef3c7,stroke:#d97706 style F5 fill:#f3e8ff,stroke:#7c3aed
认证流程(以表单登录为例):UsernamePasswordAuthenticationFilter
拦截 POST /login,从请求提取用户名密码构造
UsernamePasswordAuthenticationToken(未认证),交给 AuthenticationManager(默认 ProviderManager)。ProviderManager 遍历 AuthenticationProvider 列表,找到支持的(DaoAuthenticationProvider),后者调用 UserDetailsService.loadUserByUsername() 查用户,用 PasswordEncoder 比对密码。成功则构造已认证 Authentication(含权限),存入 SecurityContext(默认 SecurityContextHolder 持有,基于 ThreadLocal)。
授权流程(6.x):AuthorizationFilter 取代旧的 FilterSecurityInterceptor,在链末尾对每个请求做授权。它从 RequestMatcherDelegatingAuthorizationManager 找到对应 AuthorizationManager,传入 Authentication 与 Request 判定是否放行。AuthorizationManager
替代旧的
AccessDecisionManager(投票器模式),更轻量、支持方法级授权(@PreAuthorize)。
JWT 集成(无状态认证):用 OncePerRequestFilter 自定义 JwtAuthenticationFilter,从请求头解析 JWT → 校验签名/过期 → 构造 Authentication 存入 SecurityContext,后续 AuthorizationFilter 即可基于该 Authentication 授权。无需 Session,适合微服务/前后端分离。
6.x 无状态 JWT 安全配置(Lambda DSL,无 Adapter):
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启 @PreAuthorize 方法级授权(6.x)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// 6.x 用 Lambda DSL
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 在 UsernamePasswordAuthenticationFilter 前插入 JWT 过滤器
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 禁止用 MD5
}
}
架构级安全设计四原则:① 无状态优先——微服务用 JWT/Token,避免 Session 共享;② 最小权限——默认 deny all,逐个放行;③ 方法级授权兜底——@PreAuthorize 防止 URL 层漏配;④ 密码用 BCrypt/Argon2,禁止 MD5/SHA。
追问 1:6.x 为何废弃 WebSecurityConfigurerAdapter?多 SecurityFilterChain 如何工作?
废弃原因:① 组合优于继承——Adapter 是抽象类需继承,限制灵活性,@Bean SecurityFilterChain 可定义多个、可条件注入;② 多链支持更自然——一个应用可有多条链(如 /api/** 走 JWT 无状态、/admin/** 走 Session 有状态),FilterChainProxy 按 securityMatcher 顺序匹配第一条命中的链执行,多 @Bean 声明比继承多个 Adapter 直观得多;③ AOT 友好——函数式配置更易构建期分析。
追问 2:authorizeRequests 与 authorizeHttpRequests 的本质区别?
authorizeRequests(旧)用 AccessDecisionManager + 投票器(AffirmativeBased/ConsensusBased),基于 FilterSecurityInterceptor。authorizeHttpRequests(6.x 新)用 AuthorizationManager 接口,基于 AuthorizationFilter,直接返回 boolean,更简单高效,且支持基于 Bean 的自定义授权逻辑(.access((auth, req) -> customCheck()))。旧版已废弃,新项目必须用 authorizeHttpRequests。
追问 3:SecurityContext 如何在多线程与异步场景下传递?
默认 SecurityContextHolder 用 MODE_THREADLOCAL,主线程的 SecurityContext 不会自动传到子线程。解决:① DelegatingSecurityContextExecutor 包装线程池;② @Async 方法配合 SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL)(但有继承边界限制);③ Spring Security 6 提供 SecurityContextHolderStrategy + DeferredSecurityContext,支持响应式与 Virtual Threads 下的延迟上下文。微服务跨服务传递需把 Authentication 序列化进 JWT/请求头,下游解析重建。
- 6.x 仍用 WebSecurityConfigurerAdapter:已被删除,编译报错,必须改为
@Bean SecurityFilterChain。 - authorizeRequests 与 authorizeHttpRequests 混用:两者基于不同授权管理器,混用导致配置失效或冲突,6.x 统一用
authorizeHttpRequests。 - 无状态 JWT 却启用 CSRF:CSRF 防御基于 Session/Token in Cookie,JWT in Header 无需 CSRF,应
csrf().disable()。 - SecurityContext 跨线程丢失:
@Async/线程池中拿不到 Authentication,未配置DelegatingSecurityContextExecutor。 - 密码用 MD5/明文:BCrypt 才是安全实践,MD5 可被彩虹表破解。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察对
Spring Boot 如何编排从 main() 到应用就绪的完整生命周期
的架构认知。SpringApplication.run() 不是简单地调一次 refresh()——它在 IoC 容器刷新之上叠加了事件驱动启动、自动配置触发、嵌入式容器启停、Runner 回调四层编排。与 C05.01 讲的 Spring Framework refresh() 13 步(容器内幕)不同,本题聚焦Boot 层如何驱动容器、在什么时机发布什么事件、各扩展点(Initializer / Listener / Runner)分别在哪个阶段介入。理解了启动流程,就能回答"自动配置在什么时机生效""嵌入式容器何时启动""如何在 Bean 初始化前修改 Environment""ApplicationRunner 和 CommandLineRunner 区别"等一系列问题。
Spring XML 时代(2003-2009):开发者手动创建 ClassPathXmlApplicationContext,传入 XML 配置文件路径,构造器内部直接调 refresh()。没有"启动流程"概念——容器即应用,refresh()
完就绪。启动过程无事件、无回调、无自动配置,一切手动。
Spring 注解时代(2010-2013):AnnotationConfigApplicationContext 取代 XML,@Configuration + @ComponentScan 成为主流。容器启动仍是一次 refresh(),但引入了
AnnotationConfigApplicationContext.register()
延迟刷新能力。仍无统一的启动编排框架。
Spring Boot 1.x(2014):SpringApplication.run()
诞生——把"创建容器 → 准备环境 → 刷新容器 → 启动嵌入式服务器 → 执行 Runner"编排成一条流水线,并引入
7 个生命周期事件。通过 spring.factories 注册 ApplicationListener 和 ApplicationContextInitializer,实现零配置扩展。这是"启动流程"作为独立概念的起点。
Spring Boot 2.x(2018):事件体系完善——新增 ApplicationContextInitializedEvent 和 ApplicationStartedEvent,使事件链从 5 个扩展到 7 个。ApplicationRunner(接收 ApplicationArguments,参数解析更友好)与 CommandLineRunner(接收原始 String[])并存,均为启动后回调接口。spring.factories 的 EnvironmentPostProcessor 成为修改 Environment 的标准扩展点。
Spring Boot 3.x(2022):AOT 预处理 + Native Image 改变启动模型——AOT 在构建期生成 Bean 定义与配置快照,运行期跳过条件评估与类路径扫描,启动从秒级降至毫秒级。事件机制保持不变,但 spring.factories 中自动配置 key 迁移到 AutoConfiguration.imports(见 C06.01)。SpringApplication 新增 setLazyInitialization() 支持全局懒加载。
趋势:启动优化从"减少条件评估"走向"构建期冻结一切"——AOT/Native 是终极方向;同时 GraalVM Native Image 下事件链不变,但 Bean 实例化策略大变(无反射、构造器注入为主),架构师需理解 AOT 与 JIT 模式下启动流程的差异。
启动流程七阶段与七事件(SpringApplication.run()
核心时序):
推断 Web 类型 → 读 Initializer/Listener"] --> B["2. prepareEnvironment
加载 application.yml → 绑定"] B --> E1["📢 ApplicationEnvironmentPreparedEvent"] E1 --> C["3. createApplicationContext
根据 Web 类型创建容器"] C --> E2["📢 ApplicationContextInitializedEvent"] E2 --> D["4. prepareContext
注册主配置类 → 执行 Initializer"] D --> E3["📢 ApplicationPreparedEvent"] E3 --> F["5. refreshContext
refresh 13 步 → 自动配置生效
→ 嵌入式容器启动"] F --> E4["📢 ApplicationStartedEvent"] E4 --> G["6. callRunners
ApplicationRunner / CommandLineRunner"] G --> E5["📢 ApplicationReadyEvent"] style E1 fill:#fef3c7,stroke:#d97706 style E3 fill:#fef3c7,stroke:#d97706 style E5 fill:#dcfce7,stroke:#059669 style F fill:#dbeafe,stroke:#2563eb
七个事件的含义与扩展点:
| 事件 | 发布时机 | 此时容器状态 | 典型扩展点 |
|---|---|---|---|
| ApplicationStartingEvent | run() 开始、环境创建前 | 容器未创建 | 极早期初始化(注册 SPI) |
| ApplicationEnvironmentPreparedEvent | Environment 准备完成 | 容器未创建 | EnvironmentPostProcessor:修改配置源 |
| ApplicationContextInitializedEvent | 容器创建 + Initializer 执行后 | 空容器,无 Bean | 注册极早期 Bean |
| ApplicationPreparedEvent | BeanDefinition 加载完成、refresh 前 | 有定义、未实例化 | 注册 BeanFactoryPostProcessor |
| ApplicationStartedEvent | refresh 完成、Runner 前 | 所有单例 Bean 就绪 | 需在 Runner 前执行的逻辑 |
| ApplicationReadyEvent | Runner 执行完成 | 应用完全就绪 | 通知外部探活、开流量 |
| ApplicationFailedEvent | 启动异常 | 不确定 | 错误上报、优雅退出 |
三层扩展点对比:
ApplicationContextInitializer:在容器创建后、refresh 前执行(第 4 阶段),能定制 ApplicationContext 本身(如注册自定义 BeanFactoryPostProcessor)。时机比 Listener 早,但此时没有 Bean,不能用 @Autowired。注册方式:spring.factories 或 addInitializers()。
ApplicationListener:监听特定事件,在事件发布时执行。最灵活——可以监听 ApplicationEnvironmentPreparedEvent(容器都没创建时修改配置),也可以监听 ApplicationReadyEvent(应用就绪后开流量)。注册方式:spring.factories、@Component(仅容器创建后的事件)、addListener()。
ApplicationRunner / CommandLineRunner:在refresh 完成、所有 Bean 就绪后执行(第 6 阶段),能用 @Autowired 注入任意 Bean。区别:CommandLineRunner 接收原始 String[] 参数,ApplicationRunner 接收解析后的 ApplicationArguments(支持 --key=value 解析)。二者执行顺序可用 @Order 控制。
自动配置何时生效:@SpringBootApplication 包含 @EnableAutoConfiguration,后者通过 @Import(AutoConfigurationImportSelector.class) 在refresh 的第 5 步(invokeBeanFactoryPostProcessors)触发——ConfigurationClassPostProcessor 解析主配置类时发现 @Import,调用
AutoConfigurationImportSelector.selectImports()
加载候选类、条件过滤、注册 BeanDefinition。自动配置发生在
ApplicationPreparedEvent 之后、ApplicationStartedEvent
之前的 refresh 过程中(C05.01 详解了 refresh 13 步内幕)。
嵌入式容器何时启动:Web 应用在 refresh 的 onRefresh() 阶段(第 9 步)启动嵌入式容器——ServletWebServerApplicationContext.onRefresh() 调用 createWebServer(),创建 Tomcat/Undertow/Jetty 实例并绑定端口。此时 Bean 已实例化但Runner 尚未执行,容器在 finishRefresh()(第 12 步)发布 ContextRefreshedEvent 后完全就绪。
系统启动慢(40s+),通过事件链分析定位到三个瓶颈:Flyway 迁移(8s)、Redis 连接池初始化(3s)、本地缓存预热(15s)。用事件机制做了三件事:
// 1. EnvironmentPostProcessor: 启动最早期动态注入配置(如根据环境变量切换数据源)
@Order(1)
public class RentEnvPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment env,
SpringApplication app) {
// 容器未创建, 此时可安全修改 Environment
if ("prod".equals(env.getProperty("spring.profiles.active"))) {
Map<String, Object> overrides = new HashMap<>();
overrides.put("spring.datasource.hikari.maximum-pool-size", "20");
env.getPropertySources().addFirst(new MapPropertySource("prod-overrides", overrides));
}
}
}
// 注册: META-INF/spring.factories → org.springframework.boot.env.EnvironmentPostProcessor=com...RentEnvPostProcessor
// 2. ApplicationRunner: 所有 Bean 就绪后异步预热缓存 (不阻塞启动)
@Component
@Order(10)
public class CacheWarmupRunner implements ApplicationRunner {
@Autowired
private RentCacheService cacheService;
@Override
public void run(ApplicationArguments args) {
// ApplicationReadyEvent 之前执行, 此时 Bean 全部就绪
// 异步预热, 不阻塞 ApplicationReadyEvent 发布
CompletableFuture.runAsync(() -> cacheService.warmup());
}
}
// 3. ApplicationReadyEvent: 应用完全就绪, 通知网关开流量
@Component
public class ReadyEventListener {
@Autowired
private GatewayClient gateway;
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
// 只有 Runner 全部执行完, 才通知网关引流
gateway.notifyReady();
}
}
启动流程扩展四原则:① 修改 Environment 用 EnvironmentPostProcessor——此时容器未创建,改配置最安全;② 定制容器用 Initializer——比 Listener 早,能注册 BeanFactoryPostProcessor;③ 初始化数据用 ApplicationRunner——Bean 全就绪,可注入任意依赖,重活异步化不阻塞启动;④ 开流量用 ApplicationReadyEvent——确保 Runner 全部完成后才对外提供服务,避免"半就绪"状态接请求。
追问 1:SpringApplicationRunListener 是什么?和 ApplicationListener 什么关系?
SpringApplicationRunListener
是启动流程的内部编排者——SpringApplication.run() 不直接发布事件,而是委托 SpringApplicationRunListener 在每个阶段调用 starting()/environmentPrepared()/contextPrepared() 等方法,由它内部用 ApplicationEventMulticaster 广播事件给所有 ApplicationListener。默认实现 EventPublishingRunListener 通过 spring.factories 加载。关系:RunListener 是事件的发布方(SPI,框架内部),ApplicationListener
是事件的消费方(Bean 或
spring.factories 注册)。开发者通常只写 Listener,不写 RunListener——除非需要在事件发布前后插入框架级逻辑(如 Spring Cloud 的 BootstrapApplicationListener)。
追问 2:AOT 模式下启动流程有什么不同?事件链变了吗?
AOT(Ahead-of-Time)在构建期执行
SpringApplication.run() 的"模拟运行"——AOT 处理器创建 SpringApplication 实例并走到 ApplicationPreparedEvent,捕获所有 BeanDefinition、条件评估结果、@Configuration 解析结果,生成 ApplicationContextAOTGenerator 代码。运行期 run() 跳过类路径扫描和条件评估,直接用 AOT 生成的 BeanDefinition 注册。事件链不变——7 个事件照常发布,但 ApplicationEnvironmentPreparedEvent 到 ApplicationPreparedEvent 之间的耗时从数百毫秒降至接近 0(因为条件判断已冻结)。这就是 Native Image 启动从秒级降到毫秒级的核心原因。
- 在 Initializer 中注入 Bean:Initializer 在容器 refresh 前执行,此时 Bean 未实例化,
@Autowired注入的是 null。Initializer 只能操作ApplicationContext本身,不能依赖业务 Bean。 - 混淆 ApplicationStartedEvent 与 ApplicationReadyEvent:
Started在 refresh 后、Runner 前发布,此时数据可能未预热;Ready在 Runner 完成后发布。开流量、通知探活必须用Ready,否则"半就绪"接请求导致错误。 - Runner 中做阻塞操作拖慢启动:ApplicationRunner 在启动主线程同步执行,重操作(全量缓存预热、大量数据初始化)会阻塞
ApplicationReadyEvent发布,应异步化或延迟执行。 - 用 @PostConstruct 做启动初始化:
@PostConstruct在 Bean 初始化时执行,此时其他 Bean 可能未就绪,依赖顺序不确定。跨 Bean 的初始化逻辑应放 ApplicationRunner。 - EnvironmentPostProcessor 中读 Bean:此扩展点在容器创建前执行,没有任何 Bean 存在,只能操作
Environment(增删 PropertySource)。需要读数据库决定配置的场景应改用ApplicationEnvironmentPreparedEvent的异步预处理 + 延迟加载。
这道题考察对分布式 Session 方案的底层理解。Spring Session 的本质是用 Filter 拦截 HttpServletRequest.getSession(),把 Session 的存储与过期管理从 Servlet 容器(内存)解耦到外部存储(Redis/JDBC)——业务代码用 HttpSession API 无感知,但底层已变成分布式共享。理解它要回答三个问题:① 它如何"骗过"Servlet 容器(SessionRepositoryFilter
包装 Request/Response);② Session 如何在 Redis 存储与过期(Hash 结构 + TTL 续期);③ 与 JWT/OAuth 的关系与选型。
Session(Servlet 原生):Session 存在 Servlet 容器内存(如 Tomcat 的 ConcurrentHashMap),单机可用,多机需 Tomcat session 复制(DeltaManager,全量广播,不可扩展)或 Sticky Session(负载均衡绑定节点,节点宕机丢 Session)。
Spring Session(2014):用 @EnableRedisHttpSession + SessionRepositoryFilter 把 Session 存到 Redis,业务无感。解决多机共享、水平扩展。支持 Redis/JDBC/Hazelcast 多后端。
JWT 兴起(2015+):无状态 Token 方案,服务端不存 Session,Token 自带用户信息 + 签名。优势是无状态、易水平扩展、跨域;劣势是难主动吊销、续期复杂、Token 大。适合前后端分离/微服务。
双 Token + Redis 黑名单(2018+):Access Token(短)+ Refresh Token(长),Access Token 无状态快速校验,Refresh Token 存 Redis 可吊销。兼顾无状态与可控性,是当前主流。
趋势:Spring Session 仍适合传统有状态 Web 应用(管理后台、SSO);微服务/前后端分离倾向 JWT/双 Token;OAuth 2.1 + Spring Authorization Server 成为标准认证方案。两者非互斥——Spring Session 可与 OAuth2 整合存授权信息。
核心机制:SessionRepositoryFilter 包装 Request/Response。这是"骗过"Servlet 容器的关键——该 Filter 用 SessionRepositoryRequestWrapper 包装 HttpServletRequest,重写 getSession() 返回 SessionRepositoryWrapper(委托给 SessionRepository)。业务调 request.getSession().setAttribute() 实际写入 Redis,而非容器内存。
Hash 结构, TTL 30min 自动续期 App-->>Filter: 响应 Filter-->>Browser: 响应 (Cookie 回写 sessionId)
Redis 存储结构:每个 Session 用一个 Hash 存(Key:spring:session:sessions:{id}),字段含 creationTime/lastAccessedTime/maxInactiveInterval/sessionAttr:xxx。另用 spring:session:expirations:{timestamp} Set 实现过期通知。TTL 续期:Spring Session 默认不在每次访问时都调用 save()(性能考虑),而是在响应阶段通过 commitSession 判断 Session 是否变更或接近过期,变更或接近过期时才重置 Hash 的 TTL 为 maxInactiveInterval,实现"滑动过期"。可通过 saveMode=ALWAYS 强制每次访问都续期。
Session 事件:Spring Session 通过 Redis Keyspace Notifications(__keyevent@*__:expired/del)监听 Session 过期/删除,转换为 SessionExpiredEvent/SessionDeletedEvent,业务可监听做清理(如踢用户下线)。需在 Redis 开启 notify-keyspace-events Exg。
启用 Spring Session Redis:
// 1. 依赖: spring-session-data-redis + spring-boot-starter-data-redis
// 2. 启动类加注解(Boot 自动配置,通常无需手写)
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 分钟
@SpringBootApplication
public class App { }
// 3. 业务无感知,照常用 HttpSession
@GetMapping("/login")
public String login(HttpSession session) {
session.setAttribute("user", currentUser);
return "ok";
}
Session vs JWT 选型决策:
| 维度 | Spring Session(有状态) | JWT(无状态) |
|---|---|---|
| 服务端存储 | 需 Redis 存 Session | 不存,Token 自包含 |
| 主动吊销 | 容易(删 Redis Key) | 难(需黑名单,退化成有状态) |
| 水平扩展 | 需共享 Redis | 天然支持 |
| 续期 | 滑动过期简单 | 复杂(双 Token) |
| 跨域/移动端 | 需 Cookie,跨域麻烦 | Header 传 Token,友好 |
| 安全 | SessionId 不含敏感信息 | Payload 可读(仅签名防篡改) |
| 适用 | 传统 Web/管理后台/SSO | 前后端分离/微服务/移动端 |
Spring Session + OAuth 整合:用 Spring Session 管理网关登录态,OAuth2 资源服务器校验 JWT。注意三点:① Session 数据最小化——只存 userId,不存大对象,避免 Redis 压力;② 开启 Keyspace Notifications 才能收到过期事件;③ 跨域用 Spring Session Cookie 传 sessionId,移动端建议改 JWT。
追问 1:Spring Session 如何实现"滑动过期"?与 Redis 自带 TTL 有何关系?
Spring Session 的滑动过期靠两点:① 每次 getSession()(访问)会触发 SessionRepository.save(),重新 EXPIRE 该 Key 为 maxInactiveInterval,重置 TTL;② 若整个 maxInactiveInterval 内无访问,Redis TTL 到期自动删除,Session 失效。即"有访问就续期,无访问到点过期"。注意:Spring Session 默认 不每次访问都 save(性能考虑),而是通过 commitSession 在响应阶段判断 Session 是否变更(dirty),仅变更或接近过期时才续期。可通过 @EnableRedisHttpSession(saveMode=ALWAYS) 强制每次续期。
追问 2:Session 事件在多实例下如何避免重复消费?
Redis Keyspace Notifications 是发布订阅模式——所有订阅的实例都会收到同一个过期事件,若都处理会导致重复(如重复踢下线)。解决:①
事件处理幂等——清理操作设计为幂等;② 用分布式锁/Redis Set 去重——处理前 SETNX 标记已处理;③ 只让一个实例订阅——但单点风险。生产环境推荐幂等 + 去重组合。
- 误以为加依赖就生效:需
@EnableRedisHttpSession或 Boot 自动配置,且 Redis 连通,否则仍用容器内存 Session。 - Session 存大对象:存 List/大 JSON 进 Session,每次访问全量读写 Redis,性能差,应只存 userId。
-
未开启 Keyspace Notifications:过期事件不触发,
SessionExpiredEvent收不到。 - JWT 误当 Session 用:JWT 难主动吊销,退出登录后 Token 仍有效,需黑名单或短 Access Token。
- 跨域 Cookie 丢失:前后端分离跨域时 Cookie 不携带,需配置
sameSite/withCredentials或改 Token 方案。
这道题考察对 Spring Boot 生产级可观测性的理解。Actuator 的本质是一组通过 HTTP/JMX 暴露的应用运行时端点,把应用内部状态(健康、指标、配置、线程、日志)标准化对外提供,是运维与监控系统的数据源。理解它要抓住三点:① 端点体系(health/info/metrics/env/loggers/heapdump 等);② 从 1.x 到 2.x+ 的安全演进(默认全开放 → 默认仅 health/info,其余需显式开启 + 权限);③ 与 Micrometer/Prometheus 的集成做指标采集。这是高级开发线上运维必备能力。
Actuator 1.x(2014-2018):端点放在根路径(/health、/env、/metrics),默认大部分端点开放且无细粒度权限控制,安全风险高(/env 泄露配置、/heapdump 泄露内存)。端点返回 JSON 结构不统一,难以标准化采集。
Actuator 2.x(2018,安全重构):关键演进——① 端点统一移到 /actuator 前缀;② 默认仅 health 和 info 开放,其余需 management.endpoints.web.exposure.include 显式开启;③ 引入 Micrometer 替代自研 metrics,统一度量门面(类似 SLF4J 之于日志),支持 Prometheus/Atlas/Datadog 等多后端;④ /metrics 改为可按维度查询(/metrics/jvm.memory.used?tag=area:heap)。
Actuator 3.x / Boot 3(2022):① 观测 API 统一——Micrometer Observation API 统一 Metrics 与 Tracing(OpenTelemetry 兼容),并通过 traceId/spanId 注入实现 Log Correlation;② /health 支持健康组(Health Group)分组聚合;③ 端点支持 JSON + OpenMetrics 多格式;④ 与 Spring Security 深度整合,端点权限走标准 SecurityFilterChain。
趋势:可观测性向 OpenTelemetry 标准靠拢,Actuator 作为 Spring 应用的数据采集层,与 OTel Collector + Prometheus + Grafana/Jaeger 组成标准可观测性栈;eBPF 等无侵入技术兴起但 Actuator 仍是应用层指标主源。
端点体系。每个端点是一个 @Endpoint Bean,通过 @ReadOperation/@WriteOperation/@DeleteOperation
暴露 HTTP/JMX 操作:
| 端点 | 作用 | 默认开放 |
|---|---|---|
/actuator/health |
健康检查(DB/Redis/Disk 探活) | 是 |
/actuator/info |
应用信息(版本/构建信息) | 是 |
/actuator/metrics |
Micrometer 指标(JVM/HTTP/Tomcat) | 否 |
/actuator/env |
环境配置(含敏感,需脱敏) | 否 |
/actuator/loggers |
动态调整日志级别(POST) | 否 |
/actuator/heapdump |
下载堆 dump(高危) | 否 |
/actuator/prometheus |
Prometheus 抓取格式 | 否(需 micrometer-registry-prometheus) |
HealthIndicator 健康检查机制:/health 聚合所有 HealthIndicator Bean(DataSourceHealthIndicator/RedisHealthIndicator/DiskSpaceHealthIndicator),每个返回 Status(UP/DOWN/UNKNOWN),整体状态取最差。K8s liveness/readiness 探针可对接不同 health group。
Micrometer 度量门面:Actuator 的 /metrics 底层是 Micrometer MeterRegistry,类似 SLF4J。它定义 Counter/Gauge/Timer/DistributionSummary 四种计量器,通过 MeterBinder 自动绑定 JVM/HTTP/Tomcat/Process 指标。加 micrometer-registry-prometheus 依赖即暴露 /actuator/prometheus 供 Prometheus 抓取。
生产配置(安全 + Prometheus):
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers # 显式开放
exclude: env,heapdump # 禁高危
endpoint:
health:
show-details: when-authorized # 仅授权用户看详情
probes:
enabled: true # 开启 K8s liveness/readiness
metrics:
tags:
application: ${spring.application.name} # 全局标签
自定义 HealthIndicator(如检查下游依赖):
@Component
public class DownstreamHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// 检查下游服务
boolean ok = checkDownstream();
if (ok) {
return Health.up().withDetail("latency", 50).build();
}
return Health.down().withDetail("error", "timeout").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
生产 Actuator 五要点:① 最小开放——只开 health/info/metrics/prometheus,禁 env/heapdump;② health 走 SecurityFilterChain——show-details=when-authorized,K8s 探针用独立 health group 免鉴权;③ 对接 Prometheus——加 micrometer-registry-prometheus + Grafana 仪表盘;④ 自定义业务指标——用 Counter/Timer 埋点订单量/接口耗时;⑤ 端点独立端口——management.server.port
隔离管理与业务流量。
追问 1:K8s liveness 与 readiness 探针如何对接 Actuator?两者区别?
Spring Boot 2.3+ 自动配置 /actuator/health/liveness 与 /actuator/health/readiness(需 management.endpoint.health.probes.enabled=true)。liveness(存活探针)反映应用是否可恢复——DOWN 时 K8s 重启 Pod,只检查应用是否死锁/内存耗尽,不应含外部依赖(避免级联重启);readiness(就绪探针)反映是否可接流量——DOWN 时 K8s 从 Service 摘除 Pod 但不重启,可含 DB/Redis 检查(依赖不可用时摘流量)。关键:不要把 DB 健康放 liveness,否则 DB 抖动导致全 Pod 重启雪崩。
追问 2:Micrometer 与 OpenTelemetry 的关系?Boot 3 如何整合?
Micrometer 是度量门面(Metrics),Boot 3 引入
Micrometer Observation API 统一 Metrics 与 Tracing,并通过 traceId 自动注入 MDC 实现 Logging 关联(不是直接统一日志格式)。加 micrometer-tracing-bridge-otel 即把 Observation 桥接到 OpenTelemetry,通过 OTelAutoConfiguration 导出到 Jaeger/Tempo。即业务用 Micrometer API 埋点,底层桥接到 OTel 标准协议,兼顾 Spring 生态便利与 OTel 标准。这是当前可观测性主流方案。
- 2.x 默认开放所有端点:误以为像 1.x 全开放,实际默认仅 health/info,需显式 include。
- /env 泄露敏感配置:开放 env 端点且未脱敏,密码/密钥泄露,应 exclude 或用
***脱敏。 - liveness 含 DB 检查:DB 抖动致全 Pod 重启雪崩,DB 检查只放 readiness。
- 未设独立管理端口:管理端点与业务混在同一端口,易被外网访问,应
management.server.port隔离。 - 自定义 HealthIndicator 阻塞:健康检查同步调下游超时拖垮
/health,应设超时 + 异步。
这道题考察对 Spring Boot
外部化配置(Externalized Configuration)体系的整体把握。配置加载的本质是多 PropertySource 按优先级叠加,高优先级覆盖低优先级——Spring 把命令行参数、环境变量、yml、properties、随机值等抽象为有序的 PropertySource 列表,Environment
按从高到低顺序查找属性。理解优先级就能回答"为什么 yml 没生效""profile 如何覆盖""配置中心为何能覆盖本地"等一切配置问题。架构师还需掌握配置加密、敏感信息管理、多环境策略。
Spring 早期(2.x-3.0):配置靠 XML PropertyPlaceholderConfigurer 解析 ${} 占位符,单一 properties 文件。多环境靠多套文件手动切换。Spring 3.1(2011):引入 @Profile 注解和 Environment 抽象,奠定多环境支持基础。
Spring Boot 1.x:引入 application.properties + profile(application-{profile}.properties)+ 命令行参数 --spring.profiles.active。配置源扩展到命令行、环境变量、JNDI。
Spring Boot 2.x:全面支持 YAML(application.yml),引入 @ConfigurationProperties 结构化绑定。配置源进一步丰富:random、JNDI、OS env、System properties、application.yml、application.properties、@PropertySource。
Spring Boot 2.4+(配置导入):引入 spring.config.import,支持导入配置中心(Nacos/Consul/Kubernetes ConfigMap)与额外文件,且 文档化配置加载顺序(ConfigData API 重构)。spring.config.location 与 spring.config.additional-location 调整语义。
趋势:配置中心(Nacos/Apollo)成为标配,spring.config.import
统一本地与远程配置加载;配置加密(Jasypt/Vault)集成;K8s ConfigMap/Secret 作为原生配置源。
配置源优先级(从高到低,高者覆盖低者)。这是核心必背:
| 优先级 | 配置源 | 说明 |
|---|---|---|
| 1(最高) | 命令行参数 --key=value |
--server.port=9090 |
| 2 | SPRING_APPLICATION_JSON |
环境变量中的 JSON |
| 3 | ServletConfig/ServletContext 初始化参数 | Web 场景 |
| 4 | JNDI 属性 | J2EE 环境 |
| 5 | Java 系统属性 System.getProperties() |
-Dkey=value |
| 6 | OS 环境变量 | SERVER_PORT=9090,支持 relaxed binding |
| 7 | RandomValuePropertySource |
random.* |
| 8 | profile 特定的 application.yml(jar 外) | application-prod.yml |
| 9 | profile 特定的 application.yml(jar 内) | 打包进 jar 的 |
| 10 | application.yml(jar 外,无 profile) | 同目录外部文件 |
| 11(最低) | application.yml(jar 内,无 profile) | classpath 默认值 |
| - | @PropertySource |
优先级最低,且只对 properties 生效 |
Profile 机制:spring.profiles.active=prod 激活 prod profile,application-prod.yml 覆盖 application.yml。可用 spring.profiles.group(2.4+)组合多个 profile。@Profile("prod")
注解控制 Bean 按 profile 装配。
松散绑定(Relaxed Binding):server.port、SERVER_PORT、server.port(env)、serverPort(系统属性)都能绑定到 server.port 属性。规则:kebab-case(推荐)→ camelCase → snake_case → UPPER_CASE 都支持。
ConfigData 加载机制(2.4+):ConfigDataEnvironmentPostProcessor 按 spring.config.import 递归加载配置,支持 optional:file:./extra.yml(可选)、nacos:xxx.yml(配置中心)。这是统一本地与远程配置的关键。
多环境配置策略:
# application.yml (默认,开发用)
server:
port: 8080
spring:
profiles:
active: dev # 默认 dev
# application-prod.yml (生产)
server:
port: 80
spring:
config:
import: # 2.4+ 从 Nacos 拉取覆盖配置
- optional:nacos:rental-prod.yml
# 启动指定环境
# java -jar app.jar --spring.profiles.active=prod
# 或 export SPRING_PROFILES_ACTIVE=prod
配置加密:敏感信息(DB 密码/密钥)禁止明文进 yml。方案:① Jasypt——ENC(密文),启动传密钥解密;② Vault——Spring Cloud Vault 集成,密钥存 Vault 服务端;③ K8s Secret——以环境变量/挂载文件注入,yml 用 ${DB_PASSWORD} 引用。项目规范"禁止硬编码配置,统一读取 yml"即遵循此原则。
配置管理五要点:① 分层配置——默认值在 jar 内 yml,环境差异用 profile 覆盖,敏感值用环境变量/加密;② 用 @ConfigurationProperties 而非 @Value——结构化、可校验、可提示;③ 配置中心覆盖本地——spring.config.import 拉远程配置,实现热更新;④ 敏感信息加密——Jasypt/Vault/K8s Secret,禁止明文入库;⑤ 优先级验证——用 /actuator/env(脱敏后)查看最终生效值,排查覆盖问题。
追问 1:为什么命令行参数优先级最高?这样设计有何好处?
设计动机:运维可临时覆盖,无需改包。生产故障时(如端口冲突、日志级别调优),运维用
--logging.level.root=DEBUG
临时启动排查,无需重新打包发布。命令行最高优先级保证"临时干预"一定生效,且不污染配置文件。这也符合"外部化、可覆盖"原则——越靠近运行时、越易变更的源优先级越高。
追问 2:2.4 的 spring.config.import 与旧的 spring.config.location 有何区别?
spring.config.location 是替换——指定后默认 application.yml 不再加载(除非显式加 optional:classpath:/application.yml),易导致默认值丢失。spring.config.import
是追加导入——在默认加载基础上额外导入,保留默认值,且支持
optional: 前缀(缺失不报错)、配置中心 URI(nacos:)。2.4+ 推荐用 import,location 仅兼容保留。
追问 3:配置热更新如何实现?哪些能热更新哪些不能?
配置中心(Nacos/Apollo)变更后通过长轮询/监听感知,发布
RefreshEvent。@RefreshScope Bean 在下次访问时重建(销毁旧实例懒加载新的),实现热更新。能热更新:普通 @ConfigurationProperties Bean、@Value(需 @RefreshScope)。不能热更新:启动期一次性初始化的 Bean(DataSource/线程池/Redis 连接池),这类需监听 RefreshScopeRefreshedEvent 手动重建,或用 ConfigurationPropertiesRebinder。架构设计时应明确标注哪些配置支持热更新。
- 优先级记反:以为 application.yml 优先于环境变量,实际命令行 > 环境变量 > yml。
- @Value 不支持热更新:默认单例注入后不变,需
@RefreshScope才能配合配置中心热更新。 - 敏感配置明文进 yml:DB 密码/密钥明文提交 Git,应加密或用环境变量。
- spring.config.location 覆盖默认配置:指定后默认 yml 不加载,默认值丢失,应用 import 替代。
- profile 嵌套覆盖失效:
application-prod.yml必须在 jar 外同名目录或 jar 内才生效,路径不对则不加载。
配置中心热更新的"快"与"容量"必须有量化支撑——长轮询推送延迟与单节点长轮询客户端上限,是架构选型与集群规模规划的依据。
| 指标 | 参考基准(业界常见量级) | 说明 |
|---|---|---|
| 推送延迟 | 客户端 30s hold,配置变更 500ms 内推送,端到端 P99 <1s | 长轮询 + 本地缓存刷新 |
| 单节点容量 | 单 Nacos 节点 ~1w 长轮询客户端 | 受 TCP 连接 + 超时任务限制 |
| 集群容量 | 3 节点支撑 3w+ 客户端、10w+ 配置项 | 线性扩展受节点数约束 |
| 压测指标 | 1w 客户端同时长轮询:服务端 CPU <30%,推送延迟 P99 <2s | 验证服务端扛压能力 |
Nacos 长轮询(客户端 30s hold)可在 500ms 内感知变更、端到端 P99 <1s 生效;单节点约支撑 1w 长轮询客户端,3 节点集群支撑 3w+ 客户端与 10w+ 配置项;1w 客户端压测下服务端 CPU <30%、推送延迟 P99 <2s。规划集群规模时按"客户端数/单节点上限"预留冗余。(以上为业界参考基准,需以自身生产数据替换)
这道题考察对 Spring MVC 异常处理链的系统性理解。全局异常处理的本质是把散落在各 Controller 的 try-catch 收敛到统一出口,实现:① 业务异常与 HTTP 状态码的映射;② 统一错误响应体(如 ApiResponse);③ 异常日志与监控埋点;④ 隐藏堆栈防信息泄露。核心机制是 @ControllerAdvice + @ExceptionHandler,但要理解它为何能"全局拦截"——底层是 ExceptionHandlerExceptionResolver,是 Spring MVC 三大异常解析器之一。项目规范要求统一 ApiResponse<T> 封装,这道题是落地关键。
Servlet 原生:异常靠 web.xml 的 <error-page> 按 HTTP 状态码或异常类型跳错误页,无法编程式处理,只能返回静态页。
Spring MVC 早期(HandlerExceptionResolver):引入 HandlerExceptionResolver 接口,可实现 resolveException 编程式处理。内置 SimpleMappingExceptionResolver(异常类 → 视图名映射)、ResponseStatusExceptionResolver(@ResponseStatus 注解)。痛点:每个 Resolver 只能返回 ModelAndView,JSON 响应需手动写 Response。
Spring 3.2+(@ControllerAdvice + @ExceptionHandler):里程碑演进——@ControllerAdvice 全局拦截所有 Controller,@ExceptionHandler 按异常类型匹配处理方法,可直接返回 @ResponseBody/ResponseEntity。彻底取代手写 Resolver,成为主流。底层由 ExceptionHandlerExceptionResolver 驱动。
Spring 5+(ResponseStatusException + ProblemDetail):① ResponseStatusException——可直接 throw 带状态码的异常,无需定义异常类;② Spring 6 引入 ProblemDetail(RFC 7807)——标准化错误响应格式(application/problem+json),@RestControllerAdvice 可直接返回 ProblemDetail。
趋势:向 ProblemDetail(RFC 7807)标准化靠拢;与可观测性结合——异常自动打 Trace/Log;云原生下异常响应需包含 request-id 便于链路追踪。
Spring MVC 异常处理链:三个 Resolver 按顺序尝试。Controller 抛出异常后,DispatcherServlet
依次调用三个 Resolver,第一个成功处理的生效:
处理 @ExceptionHandler(最高优先级)"] R1 -- 匹配到 --> H1["@ControllerAdvice 的 @ExceptionHandler 方法处理"] R1 -- 未匹配 --> R2["2、ResponseStatusExceptionResolver
处理 @ResponseStatus 注解"] R2 -- 匹配到 --> H2["按注解 status/code 返回"] R2 -- 未匹配 --> R3["3、DefaultHandlerExceptionResolver
处理 Spring 标准异常"] R3 -- 匹配到 --> H3["如 NoSuchRequestHandlingMethodException → 404"] R3 -- 未匹配 --> F["交给容器 /error 端点(Boot 默认错误页)"] style R1 fill:#dbeafe,stroke:#2563eb style H1 fill:#dcfce7,stroke:#059669
@ControllerAdvice 如何全局拦截:@ControllerAdvice 本质是一个特殊 @Component,启动时被 RequestMappingHandlerAdapter 扫描,其 @ExceptionHandler 方法注册到 ExceptionHandlerMethodResolver,建立"异常类型 → 处理方法"映射。Controller 抛异常时,ExceptionHandlerExceptionResolver 先查 Controller 内的 @ExceptionHandler,未命中再查所有 @ControllerAdvice。异常类型按继承关系最近匹配(抛 BusinessException 优先匹配 BusinessException 而非其父 Exception)。
统一全局异常处理 + ApiResponse 封装:
// 1. 统一响应体(项目规范)
@Data
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
public static <T> ApiResponse<T> error(int code, String msg) {
return new ApiResponse<>(code, msg, null);
}
}
// 2. 业务异常基类
@Getter
public class BizException extends RuntimeException {
private final int code;
public BizException(int code, String msg) { super(msg); this.code = code; }
}
// 3. 全局异常处理(@RestControllerAdvice = @ControllerAdvice + @ResponseBody)
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 业务异常: 返回业务码,不打印堆栈
@ExceptionHandler(BizException.class)
public ApiResponse<Void> handleBiz(BizException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return ApiResponse.error(e.getCode(), e.getMessage());
}
// 参数校验异常: 400
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return ApiResponse.error(400, msg);
}
// 兜底异常: 500,打印堆栈,隐藏细节
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleAll(Exception e) {
log.error("系统异常", e); // 打印完整堆栈到日志
return ApiResponse.error(500, "系统繁忙,请稍后重试"); // 不泄露堆栈
}
}
全局异常处理五要点:① 分层异常体系——BizException(业务)+ SysException(系统)+ 参数校验异常,分别处理;② 统一 ApiResponse 封装——所有异常返回统一结构,前端按 code 处理;③ 业务异常不打堆栈——预期内的用 warn,非预期的用 error 带堆栈;④ 隐藏敏感信息——兜底异常返回"系统繁忙",不泄露 SQL/堆栈;⑤ 加 TraceId——异常响应含 request-id,便于链路排查。
追问 1:@ControllerAdvice 与 @RestControllerAdvice 的区别?如何限定拦截范围?
@RestControllerAdvice = @ControllerAdvice + @ResponseBody,处理方法返回值直接序列化为 JSON,是 REST API 首选。@ControllerAdvice 返回视图,适合传统 MVC。限定范围:@ControllerAdvice("com.example.web")(按包)、@ControllerAdvice(annotations = RestController.class)(按注解)、@ControllerAdvice(basePackageClasses = ApiController.class)。多 Advice 按 @Order 排序,异常按继承最近匹配。
追问 2:Spring 6 的 ProblemDetail(RFC 7807)相比自定义 ApiResponse 有何优势?
ProblemDetail 是标准化错误响应格式(RFC 7807),字段固定:type/title/status/detail/instance,Content-Type 为 application/problem+json。优势:① 标准化——跨语言/跨服务统一,API 网关/客户端可通用解析;② Spring 原生支持——ProblemDetail.forStatusAndDetail() 直接构造,@ExceptionHandler 可返回 ProblemDetail;③ 可扩展——properties 字段放业务扩展信息。劣势:与既有 ApiResponse 体系不兼容,迁移有成本。新项目推荐 ProblemDetail,存量项目可保持 ApiResponse 但内部对齐 RFC 字段语义。
追问 3:异常处理与事务的关系?@Transactional 方法抛异常会回滚吗?
会,但要看异常类型与 rollbackFor 配置。@Transactional 默认只对 RuntimeException 和 Error 回滚,Checked Exception 不回滚(除非 rollbackFor = Exception.class)。异常处理流程:Service 抛异常 → 事务拦截器检测是否回滚 → 标记回滚 → 异常继续抛出 → @ExceptionHandler 拦截处理。注意:若在 @ExceptionHandler 中"吞掉"异常(catch 不重抛),事务已回滚但前端收到成功响应,造成数据与响应不一致。正确做法:业务异常要回滚就让它继承 RuntimeException,且 @ExceptionHandler 返回错误响应而非"吞异常"。
-
异常类型匹配过宽:
@ExceptionHandler(Exception.class)放在最前,把 BizException 也吞了,应按子类到父类顺序声明。 - 业务异常打全堆栈:预期业务异常用 error 打堆栈,刷爆日志,应 warn 仅记关键信息。
- 兜底异常泄露堆栈:把 e.getMessage() 直接返回前端,泄露 SQL/内部结构,应返回通用提示。
- 异常处理吞掉异常致事务不回滚:catch 后不重抛,事务已回滚但前端收到成功,数据不一致。
- Controller 内 @ExceptionHandler 优先于全局:局部 Handler 屏蔽全局,排查时易忽略,应避免局部 Handler。
这道题考察对声明式数据校验体系的理解。Spring Validation 的本质是把校验逻辑从业务代码中解耦,用注解声明规则,由框架在方法调用/请求绑定阶段自动执行。底层是 Bean Validation(JSR-303/380)标准 + Hibernate Validator 实现,Spring 用
MethodValidationPostProcessor(校验 Service 方法参数)和 RequestResponseBodyMethodProcessor(校验 @RequestBody)两个切入点集成。理解它要回答:① @Valid 与 @Validated 区别;② 校验失败异常如何被全局处理;③ 分组校验、自定义校验器、级联校验。
JSR-303(Bean Validation 1.0,2009):确立 @NotNull/@Size/@Min 等标准注解 + Validator 接口。Hibernate Validator 是参考实现。Spring 3 集成 @Valid 校验 @RequestBody。
JSR-349(Bean Validation 1.1,2013):支持方法级校验(参数/返回值),@Validated 注解。Spring 4 集成 MethodValidationPostProcessor,可在 Service 方法参数加注解自动校验。
JSR-380(Bean Validation 2.0,2017)/ Hibernate Validator 6:新增 @Email/@NotEmpty/@NotBlank 等注解,支持 Java 8 日期类型(@Past/@Future),支持 Stream/Optional 校验。
Bean Validation 3.0(2020,Jakarta):包名从 javax.validation 迁移到 jakarta.validation(Jakarta EE 9),Spring Boot 3 强制使用。功能基本无变化,主要是命名空间迁移。
趋势:校验仍以注解声明式为主流;与 Spring AI 结合做 LLM 输出结构校验;与 ProblemDetail 结合标准化校验错误响应。
两个切入点:请求体校验 vs 方法级校验。
1. 请求体校验(@Valid):在 Controller 方法参数 @RequestBody @Valid UserDTO dto 上加 @Valid,RequestResponseBodyMethodProcessor 在反序列化后调用 Validator.validate(),失败抛 MethodArgumentNotValidException。
2. 方法级校验(@Validated):类上加 @Validated,方法参数加 @NotNull 等,MethodValidationPostProcessor
用 CGLIB 创建代理拦截方法调用,参数校验失败抛
ConstraintViolationException。注意两种异常类型不同,全局处理要分别处理。
@Valid vs @Validated 区别:
| 维度 | @Valid(JSR 标准) | @Validated(Spring 扩展) |
|---|---|---|
| 来源 | jakarta.validation | org.springframework.validation |
| 分组校验 | 不支持 | 支持 @Validated(Update.class) |
| 使用位置 | 方法参数、字段(级联) | 类、方法、参数 |
| 方法级校验 | 不支持 | 支持(配合 @NotNull 等校验方法参数) |
| 失败异常 | MethodArgumentNotValidException | ConstraintViolationException |
校验执行原理:Hibernate Validator 用注解解析 + 反射。启动时 AnnotationConfiguration 解析 Bean 上的校验注解,构建 BeanDescriptor(含每个字段的 ConstraintDescriptor)。validate()
时反射读取字段值,对每个约束调用
ConstraintValidator.isValid(),失败收集为 ConstraintViolation。
分组校验(新增/更新用不同规则):
// 1. 分组接口
public interface Create {}
public interface Update {}
// 2. DTO 按分组标注
@Data
public class UserDTO {
@Null(groups = Create.class) // 新增时 id 必须为空
@NotNull(groups = Update.class) // 更新时 id 必须有
private Long id;
@NotBlank(groups = {Create.class, Update.class})
@Size(max = 50)
private String name;
@Email(groups = {Create.class, Update.class})
private String email;
}
// 3. Controller 指定分组
@PostMapping
public ApiResponse<Void> create(@RequestBody @Validated(Create.class) UserDTO dto) { ... }
@PutMapping
public ApiResponse<Void> update(@RequestBody @Validated(Update.class) UserDTO dto) { ... }
自定义校验器(如手机号校验):
// 1. 自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 校验逻辑(必须实现 ConstraintValidator)
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // @NotBlank 负责 null 检查,职责单一
return PATTERN.matcher(value).matches();
}
}
// 3. 使用
@Phone
private String phone;
校验四要点:① Controller 用 @Validated 分组校验——区分新增/更新规则;② Service 方法用 @Validated + 参数注解——防止绕过 Controller 直接调 Service;③ 自定义校验器职责单一——只校验格式不校验 null(交给 @NotNull);④ 两类异常分别全局处理——MethodArgumentNotValidException(请求体)与 ConstraintViolationException(方法级)异常类型不同,都要有 @ExceptionHandler。
追问 1:@Valid 与 @Validated 在级联校验中如何配合?
级联校验(对象内嵌套对象)用 @Valid 标注字段触发级联:class OrderDTO { @Valid @NotNull AddressDTO address; },校验 OrderDTO 时会递归校验 address 内的约束。注意:级联用 @Valid(JSR 标准),分组用 @Validated(Spring 扩展),两者职责不同。若嵌套对象也要分组校验,需在字段上用 @Validated(Group.class)(Spring 特有),但兼容性较差,一般用 @Valid 默认分组级联。
追问 2:方法级校验为何需要 @Validated 注解在类上?原理是什么?
方法级校验靠 MethodValidationPostProcessor 创建 CGLIB 代理,该后置处理器只对标注 @Validated 的 Bean 创建代理(pointcut 匹配 @Validated 注解)。代理在方法调用前后插入 MethodValidationInterceptor,用 ExecutableValidator.validateParameters() 校验参数。若类不加 @Validated,参数上的 @NotNull 不会生效——因为没创建代理,注解只是元数据无人执行。这是 Spring AOP 的通病:注解要生效必须有代理拦截。注意:Controller 的 @Validated 由 RequestMappingHandlerAdapter 特殊处理,不一定需要 CGLIB 代理。
追问 3:校验失败异常如何统一转为 ApiResponse?两种异常如何合并处理?
两种异常:MethodArgumentNotValidException(@Valid @RequestBody)与 ConstraintViolationException(@Validated 方法级)。前者字段错误在 getBindingResult().getFieldErrors(),后者在 getConstraintViolations()。统一处理:分别写 @ExceptionHandler,内部提取字段名+消息拼接为统一错误信息,返回 ApiResponse.error(400, msg)。也可写工具方法统一提取,避免重复。注意 Boot 3 下 ConstraintViolationException 来自 jakarta.validation,包名与 2.x 的 javax.validation 不同,import 别搞错。
- @Valid 与 @Validated 混用:需分组用 @Validated,级联用 @Valid,混用导致分组失效或级联不触发。
- Service 方法校验未加 @Validated:类上不加 @Validated,参数注解不生效(无代理拦截)。
- 自定义校验器校验 null:与 @NotNull 职责重叠,且 null 时返回 false 会导致 @NotBlank 场景重复报错,应放行 null。
- 两类异常只处理一种:只处理 MethodArgumentNotValidException,方法级校验失败抛 ConstraintViolationException 走兜底,前端收不到友好提示。
- Boot 3 包名未迁移:仍 import javax.validation,编译报错,必须改 jakarta.validation。
这道题考察对 Spring Security 6 安全架构与 OAuth2/JWT 工程落地的体系化理解。核心要回答四个层面:① SecurityFilterChain 过滤器链模型如何取代旧的 WebSecurityConfigurerAdapter;② OAuth2 授权码模式 + PKCE 的完整流程与适用场景;③ JWT 无状态认证与 Session 有状态认证的选型权衡;④ 微服务下 Token 传递、网关统一鉴权与 Token 吊销策略。理解关键在于:Spring Security 本质是一条可编排的过滤器链,认证(你是谁)与授权(你能做什么)是两个解耦阶段,OAuth2 解决的是跨域委托授权,JWT 解决的是无状态凭证自包含。
Spring Security 4(2014):XML 配置为主,<http> 命名空间声明式配置,WebSecurityConfigurerAdapter 引入 Java Config 但仍需继承重写。功能完整但配置繁琐、扩展性差。
Spring Security 5(2017):WebSecurityConfigurerAdapter 成为主流,configure(HttpSecurity) 重写成为标准姿势。5.0 引入 OAuth2 客户端支持(spring-security-oauth2,后废弃),5.2+ 重写为 spring-security-oauth2-client/-resource-server,引入 oauth2Login()。
Spring Security 6 / Spring Boot 3(2022):强制移除 WebSecurityConfigurerAdapter,全面改为 Lambda DSL(http.csrf(c -> c.disable()));SecurityFilterChain 声明为 Bean;WebSecurityCustomizer 取代 web.ignoring();包名 javax.servlet → jakarta.servlet。
OAuth2 Authorization Server(Spring 官方):原 spring-security-oauth(社区版)停止维护,Spring 官方推出 spring-authorization-server(1.0 于 2022 GA),支持 Authorization Code/Client Credentials/Device Code/Refresh Token,PKCE 内置。
Spring Boot 3.2+(2023):OAuth2 客户端/资源服务器 Starter 自动配置进一步简化,spring.security.oauth2.resourceserver.jwt
一行配置接入 JWT 资源服务器;One-Time Token Login、Passkey 等新特性加入。
1. SecurityFilterChain 过滤器链:Spring Security 的核心是一条由 Filter 组成的链。DelegatingFilterProxy(Servlet 容器注册)→ FilterChainProxy(Spring Bean)→ 匹配到的 SecurityFilterChain → 依次执行 SecurityContextHolderFilter、UsernamePasswordAuthenticationFilter、AuthorizationFilter 等约 15 个过滤器。6.x 中 FilterSecurityInterceptor 被 AuthorizationFilter 取代,授权逻辑前置。可声明多个 SecurityFilterChain,按 securityMatcher 顺序匹配,顺序决定优先级。
2. 认证核心组件:AuthenticationManager(认证入口,委托给 ProviderManager → AuthenticationProvider)→ UserDetailsService/UserDetails(加载用户)→ PasswordEncoder(BCrypt 校验)→ SecurityContext(存入 SecurityContextHolder,6.x 默认 DeferredSecurityContext 异步优化)。
3. OAuth2 授权码模式 + PKCE:用户访问客户端 → 重定向到授权服务器登录 → 授权服务器返回 code(带 redirect_uri)→ 客户端用 code + client_secret(机密客户端)换 access_token → 用 token 访问资源服务器。PKCE(Proof Key for Code Exchange)为公开客户端(SPA/移动端)增加一层:客户端先生成 code_verifier(随机串)→ 算 code_challenge(S256 哈希)随授权请求发出 → 换 token 时带 code_verifier,授权服务器校验哈希匹配,防止授权码被截获后冒用。
4. JWT 无状态认证:资源服务器用 NimbusJwtDecoder(JwtDecoder
实现)验签(RSA/EC 公钥或 JWK Set URI),验签通过后
JwtAuthenticationConverter 将 claims 转为 Authentication。JwtEncoder 用私钥签发。JWT 是签名(防篡改)非加密(claims 明文)。
5. 方法级安全:@EnableMethodSecurity(6.x 取代 @EnableGlobalMethodSecurity)开启,@PreAuthorize/@PostAuthorize 用 SpEL 做权限判断,底层 AOP 代理拦截。
SecurityFilterChain 配置示例(6.x Lambda DSL + JWT 资源服务器):
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 6.x 取代 @EnableGlobalMethodSecurity,默认开启 @PreAuthorize
public class SecurityConfig {
// 1. 安全过滤器链:6.x 改为 Bean,不再继承 Adapter
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Lambda DSL:6.x 唯一支持的写法
.csrf(csrf -> csrf.disable()) // 无状态 API 关闭 CSRF(CSRF 防的是 Cookie 伪造)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// JWT 资源服务器:验签用 JWK Set URI(授权服务器公钥)
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
.build();
}
// 2. JWT 解码器:从授权服务器拉取 JWK Set 验签
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json").build();
}
// 3. 密码编码器:BCrypt,strength 建议 10~12
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// 方法级权限:SpEL,6.x 默认启用
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #dto.userId == authentication.principal.id")
public Order update(OrderDTO dto) { ... }
}
高级开发:① SecurityFilterChain 配置——按 Lambda DSL 声明 Bean,区分公开/认证/角色接口;② OAuth2 Login 集成——oauth2Login() 接入 GitHub/Google,自动处理授权码交换与用户信息拉取;③ JWT 签发与验证——授权服务器用 JWKSource 签发,资源服务器用 NimbusJwtDecoder 验签,自定义 JwtAuthenticationConverter 映射权限。
架构师:
① 无状态 vs 有状态认证选型:单体/管理后台用 Session(CSRF 防护天然、踢人即时);微服务/API 网关用 JWT(无状态、易水平扩展)。混合方案:网关 Session + 内部服务 JWT。
② 微服务 Token 传递与网关统一鉴权:网关(Spring Cloud Gateway)统一验签,下游服务通过 X-User-Id/X-User-Roles
头接收已认证身份(网关到服务网络隔离,防止绕过)。或令牌透传,各服务自行验签(去中心化但重复)。
③ OAuth2 Authorization Server 搭建:用 spring-authorization-server,配置 RegisteredClientRepository(客户端注册)、JWKSource(RSA 密钥对)、AuthorizationServerSettings。生产环境密钥用 KMS/Vault 管理,支持密钥轮转。
④ Token 刷新与吊销策略:access_token 短期(15~30 分钟)+ refresh_token 长期(7~30 天);吊销用黑名单/版本号(Redis 记录 jti),或短 TTL + 宽限期容忍;refresh_token 一次性使用(rotation)防重放。
无状态 JWT 适合读多写少、可容忍短期吊销延迟的 API;有状态 Session 适合强吊销、即时踢人的后台系统。微服务下推荐"网关统一验签 + 下游透传身份头",避免每个服务重复验签造成性能与一致性负担。Token 切勿放 localStorage(XSS 可窃取),优先 HttpOnly Cookie(配 SameSite 防 CSRF)。
追问 1:JWT 为什么不能主动吊销?如何实现?
JWT 设计为无状态自包含,签发后服务端不存副本,到期前一直有效——这是无状态的代价。实现吊销的方案:①
黑名单:Redis 存被吊销的 jti(JWT ID),资源服务器验签后多查一次 Redis(破坏纯无状态,但 TTL 内有效);② 短 TTL + Refresh Token:access_token 设 15 分钟,吊销时让 refresh_token 失效,最迟 15 分钟后失效;③ Token 版本号:用户/会话维度维护版本,JWT 中带 ver claim,改密码/踢人时版本+1,旧 token 失效;④ introspection(RFC 7662):资源服务器每次请求问授权服务器 token 是否有效(参考令牌,完全有状态)。生产推荐"短 TTL + 黑名单关键场景(登出/改密)"组合。
追问 2:PKCE 解决了什么问题?
PKCE 解决公开客户端(无法安全保存 client_secret 的 SPA/移动 App)授权码被截获的问题。传统授权码模式靠 client_secret 防止 code 被截获后冒用,但 SPA/原生 App 无法安全存储 secret(反编译/前端可见)。PKCE 流程:客户端生成随机 code_verifier,算 code_challenge = BASE64URL(SHA256(verifier)),授权请求带 challenge → 换 token 时带 verifier → 授权服务器校验 SHA256(verifier) == challenge。攻击者截获 code 也拿不到 verifier,无法换 token。OAuth 2.1 强制所有客户端使用 PKCE,Spring Authorization Server 默认开启。
追问 3:Spring Security 6 为什么废弃 WebSecurityConfigurerAdapter?
① 继承重写耦合度高:Adapter 要求继承并重写 configure(HttpSecurity),父类方法签名/默认行为成为 API 契约,改动易破坏向后兼容;② 多安全链配置笨拙:一个 Adapter 实例只对应一条链,多链需多个 Bean 或 @Order,语义不清;③ 组件化演进方向:6.x 全面走 Bean + Lambda DSL,SecurityFilterChain 直接声明为 Bean,配置即组装,更符合 Spring Boot 自动装配哲学;④ 移除"默认开启"的隐式行为:Adapter 隐式启用一堆过滤器,新方式显式声明,行为可预测。本质是从"继承扩展"转向"组合配置",降低耦合、提升可测试性。
- 误以为 JWT 是加密:JWT 默认只签名(JWS,防篡改),payload 是 Base64URL 明文,敏感信息(密码/身份证)不能放 claim;要加密须用 JWE,复杂且少见。
- 忽略 Token 过期与刷新机制:只配 access_token,到期后 401 全屏;或 refresh_token 永不过期形成永久凭证。应 access 短期 + refresh 长期 + rotation。
- SecurityFilterChain 顺序错误导致绕过:多条链按声明顺序匹配,把宽泛的
anyRequest链放前面会吞掉后面的精确匹配;或securityMatcher写错导致/api/**命中放行链。务必用@Order显式排序并自测。 - 混淆 OAuth2 认证与授权:OAuth2 最初是授权委托协议(让第三方代为访问资源),用 OAuth2 做"登录认证"是 OIDC(OpenID Connect)的职责(在 access_token 外加 id_token)。直接用 access_token 当登录凭证存在安全隐患。
- 放行静态资源仍走安全链:6.x 应用
web.ignoring()的资源应改用WebSecurityCustomizerBean,否则每个静态请求都进 FilterChainProxy 增加开销。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察 应用生命周期与编排平台(K8s)的协同:如何让 Pod 在滚动更新/缩容时被"先摘流量、再处理完在途请求、最后退出",杜绝 5xx 与连接中断。本质是应用侧优雅停机与平台侧流量摘除时序的对齐问题。
暴力 kill:直接 SIGKILL,在途请求全部断,连接池/事务半途而废。
JVM Shutdown Hook:捕获 SIGTERM 做清理,但 Web 容器仍在接新请求,时序不可控。
Spring Boot 2.3+ Graceful Shutdown:server.shutdown=graceful,停止接收新请求、等待活跃请求在超时内完成。3.4 起默认开启优雅停机(所有内嵌 Web 服务器),无需显式配置。
K8s 探针协同:readiness(就绪,失败即摘流量)+ liveness(存活,失败重启)+ terminationGracePeriodSeconds + PreStop,把"摘流量"与"停机"解耦为可控时序。
应用侧(Spring Boot):graceful shutdown 下,容器先停止接收新连接,已接收的请求在 spring.lifecycle.timeout-per-shutdown-phase 内处理完;SmartLifecycle 控制 Bean 停止顺序,@PreDestroy 释放资源(连接池、调度器、MQ 消费者暂停)。
平台侧(K8s)时序:① Pod 被标记为 Terminating;② preStop hook 与 Endpoint 摘除并发执行(K8s 不等 preStop 完成才摘 Endpoint);③ preStop 执行完毕后发送 SIGTERM;④ 在 terminationGracePeriodSeconds(默认 30s)内等待应用优雅停机;超时则 SIGKILL。preStop 中 sleep 的目的是给 Endpoint/负载均衡传播留时间,确保摘流先于 SIGTERM 生效。
必须满足 graceful timeout < terminationGracePeriodSeconds。preStop 与 Endpoint 摘除并发启动,preStop 完成后才发 SIGTERM。否则 K8s 在应用处理完前就 SIGKILL,在途请求被截断。实际时序:Terminating → preStop(sleep) 与 Endpoint 摘除并发 → preStop 完毕 → SIGTERM → 优雅停机 → 退出。
配置示例:server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase=20s;K8s terminationGracePeriodSeconds=30、readinessProbe 探活、preStop: exec sleep 5。
资源清理清单:暂停 MQ 消费(如 RabbitListener 容器 stop)、停定时任务(Scheduler shutdown)、连接池优雅关闭(HikariCP 默认随 Bean 销毁)。
Provider 端同样需要:Dubbo/gRPC 服务需先取消注册到注册中心再停机,避免 Consumer 仍路由到已下线实例。
追问 1:readiness 与 liveness 用同一个探针会怎样? 若合一,应用假死(依赖故障)会被 K8s 不断重启而非摘流,雪上加霜;二者必须分离——readiness 反映"能否接流量",liveness 反映"进程是否该重启"。
追问 2:长任务(如文件导出)优雅停机如何处理? graceful 超时内完不成的,应转异步/任务队列,停机前把任务状态持久化,由新实例续跑,而非阻塞退出。
追问 3:滚动更新 vs 蓝绿如何选? 滚动更新资源省但流量逐步切换、风险点分散;蓝绿双环境瞬时切换、易回滚,但资源翻倍。核心系统建议蓝绿 + 优雅停机双保险。
- graceful timeout ≥ grace period:应用还没处理完就被 SIGKILL,在途请求断。
- 只配 liveness 不配 readiness:滚动时仍接流量,请求打到正在退出的实例 → 5xx。
- 关停机未停 MQ/调度:消费/任务执行到一半被强杀,数据不一致。
- 缺 PreStop:LB/Service 还没摘流就 SIGTERM,瞬间请求失败。
- Provider 未先注销:注册中心仍路由到已下线实例,Consumer 报错。
上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。
先给可观测证据与门禁,再谈实现细节;避免只报组件名。
这道题考察 云原生场景下 Java 技术选型判断力:Quarkus / Micronaut 以"编译期处理 + 原生镜像"实现极低内存与毫秒级启动,适配 K8s / Serverless / FaaS;而 Spring Boot 以生态完备与开发体验见长。核心是启动速度/内存 vs 生态成熟度的权衡。
Spring Boot 主导(2014+):运行时反射 + 条件化 Bean,生态无敌但启动慢、内存高。
云原生诉求:容器高密度、Serverless 按需冷启、FaaS 毫秒计费的场景,倒逼"快启动 + 小内存"。
新选手:Quarkus(Red Hat,build-time 扩展 + 原生优先)、Micronaut(Object Computing 主导,编译期 DI/AOP,API 近似 Spring);二者均拥抱 GraalVM Native Image。
Spring 反击:Spring Boot 3 引入 AOT(Ahead-Of-Time)处理 + spring-boot-maven-plugin build-image 支持原生镜像,缩小差距。
Spring Boot 3.x 版本演进:① 3.0(2022-11):Java 17 基线 + Jakarta EE 迁移(javax→jakarta)+ AOT/Native Image 原生支持 + Micrometer Observation API 统一可观测性 + 声明式 HTTP 客户端(@HttpExchange)+ RFC 7807 Problem Details 标准错误响应;② 3.1(2023-05):Docker Compose 集成 + @ServiceConnection(Testcontainers 零配置)+ Spring Authorization Server 生产可用;③ 3.2(2023-11):虚拟线程支持(JDK 21,spring.threads.virtual.enabled=true)+ RestClient(RestTemplate 现代替代)+ JdbcClient + SSL Bundle 热重载 + CDS 启动加速;④ 3.3(2024-05):SBOM Actuator 端点(CycloneDX 格式,供应链安全透明度)+ 可观测性零配置扩展至 Spring Data 等组件 + WebSocket 虚拟线程支持 + Base64 资源内联配置 + SSL SNI 支持 + Antora 文档重构;⑤ 3.4(2024-11):原生结构化日志(JSON 日志开箱即用)+ 优雅停机默认开启(所有内嵌 Web 服务器)+ RestClient/RestTemplate 自动配置多 HTTP 客户端(Apache HC / Reactor Netty / JDK HttpClient)+ Actuator 端点访问控制重构 + ARM 镜像构建 + MockMvcTester(AssertJ 风格);⑥ 3.5(2025-05):3.x 系列最终小版本(此后进入 4.x 代际),Bean 后台初始化自动配置(并行 ApplicationContext 启动)+ 全局 HTTP 连接器配置 + Profile 命名规则收紧 + GraalVM Native Image 支持扩展。3.x 系列从"Java 17 基线 + 云原生"奠基,到虚拟线程、结构化日志、SBOM 安全透明度逐步演进,持续缩小与 Quarkus/Micronaut 的启动/内存差距。
Spring Boot 4.0(2025-11-20 GA):已正式发布(非“计划中”)。建立在 Spring Framework 7 之上,主线包括:① 模块化(更小、更聚焦的 starter/jar,按需依赖);② 组合件级 JSpecify 空安全;③ Java 25 一等支持(仍兼容 Java 17);④ API Versioning 与 HTTP Service Client 等 REST 能力增强。升级属主版本,需对照官方 Migration Guide(Jackson 2 弃用形态、EnvironmentPostProcessor 包名迁移等)。
Spring Boot 4.1.0(2026-06-10 GA):4.x 线小版本增强——gRPC 自动配置、HTTP Client SSRF 缓解(InetAddressFilter)、可观测/OTel 依赖升级、Kotlin 2.3 等。注意:Spring Boot 3.5 OSS 于 2026-06-30 EOL,存量 3.x 需规划升级或商业补丁窗口。生产选型:新系统优先 4.1;Native/AOT 与可观测路径仍以本篇 C06.02/C06.07 与 C10.22 为准。
| 维度 | Spring Boot | Quarkus | Micronaut |
|---|---|---|---|
| DI/AOP 时机 | 运行时反射 | build-time 扩展 | 编译期(无运行时反射) |
| 原生镜像 | 3.x/4.0 AOT 支持 | 原生优先,最成熟 | 支持,编译期天然友好 |
| 启动/内存 | 较重 | ms 级启动、MB 级内存 | 接近 Quarkus |
| 生态/开发体验 | 最完备 | 较新,扩展开源 | 近似 Spring,易迁移 |
| 典型场景 | 传统微服务/内部系统 | Serverless/K8s/边缘 | 云原生微服务 |
原生镜像的代价:AOT 在构建期确定对象图,反射/动态代理/JNI/序列化需提前配置(reflect-config.json 等),且丧失运行时 JIT 峰值优化——长 CPU 密集任务峰值常低于 JIT 模式。
选型本质是场景驱动:高密度/短生命周期/冷启敏感 → Quarkus/Micronaut 原生;生态依赖多/长事务/峰值性能敏感 → Spring Boot。Spring Boot 3/4 AOT 已能在多数场景兼顾,不必盲目换栈;新系统可优先评估 Boot 4.0。
迁移路径:Micronaut API 与 Spring 高度相似(@Singleton/@Inject),老 Spring 项目可渐进迁移;Quarkus 提供 Spring API 兼容层(quarkus-spring-* 扩展)降低改造成本。
Spring Boot 原生化:mvn spring-boot:build-image 生成 Native Image;注意排查不支持的反射(用 @RegisterReflectionForBinding 声明)。
决策清单:① 是否 Serverless/FaaS 冷启敏感?② 是否 CPU 密集长任务(否决原生)?③ 是否重度依赖暂不支持原生的 starter?三项决定选型。
追问 1:原生镜像峰值性能为何常低于 JIT? 原生镜像是 AOT,无运行时基于 Profile 的激进优化(分层编译、逃逸分析驱动的锁消除/标量替换、向量化),长任务峰值偏弱。
追问 2:Spring Boot 3 AOT 能追上 Quarkus 吗? 启动/内存接近,但 Quarkus 的 build-time 体系更彻底、Dev Services/Kubernetes 原生集成更深;AOT 仍属"尽力逼近"。
追问 3:Micronaut 与 Quarkus 的核心差异? Micronaut 编译期 DI 通用性强、易嵌入;Quarkus 针对 Kubernetes/原生极致优化、扩展生态偏 Red Hat 栈。
- 盲目追求原生:反射/动态代理/序列化需大量提前配置,迁移成本被严重低估。
- 以为原生峰值更强:AOT 无运行时 Profile 优化,CPU 密集长任务峰值常低于 JIT。
- 忽略构建时间:Native build 分钟级,CI 时间翻倍,需评估发布效率。
- Spring Boot 3 AOT 兼容性假设:并非所有 starter/三方库都支持原生,踩坑在反射配置。
- 为换而换:传统长连接/重生态微服务强行迁 Quarkus,收益不足以覆盖改造成本。
这道题考察可维护的质量金字塔在云原生下的变形:纯 mock 集成测易漂;打真集群太慢。Testcontainers 提供「真依赖、可抛环境」;契约测试锁住服务间 API;ArchUnit/架构守护锁住分层与依赖规则。三者分别防:环境漂移、接口背刺、结构腐化。
单测 + 手工联调 → Spring MockMvc/切片测 → Testcontainers(Docker 真依赖)→ Pact/Spring Cloud Contract(消费者驱动契约)→ ArchUnit(代码结构即测试)。驱动力:微服务爆炸、CI 成本、回归不可见。
| 层级 | 验证什么 | 典型工具 | 速度/保真 |
|---|---|---|---|
| 单元 | 纯逻辑 | JUnit/Mockito | 最快 / 低保真 |
| 组件+真依赖 | 仓储/消息/缓存行为 | Testcontainers | 中 / 高保真 |
| 契约 | 服务间 API 兼容 | Pact / SCC | 快 / 接口保真 |
| 架构守护 | 包依赖/分层/命名 | ArchUnit | 快 / 结构保真 |
| 端到端 | 关键旅程 | 少量流水线 | 慢 / 最高 |
原则:能下沉的不要上浮;E2E 只保留黄金路径;契约失败应阻断合并。
1. Testcontainers:MySQL/Redis/Kafka 用官方模块;复用容器(reuse)降 CI 时间;与 @ServiceConnection(Boot 3.1+)集成减少手工属性。
2. 契约测试:消费者定义期望 → 提供者 CI 验证;破坏性变更走版本化 API。勿用契约替代业务断言。
3. 架构守护:禁止 domain → infrastructure 反向依赖;控制中心/api 包边界;规则进主构建,失败即红。
4. 组合:PR 门禁 = 单测 + ArchUnit + 契约;夜间 = Testcontainers 全量;预发 = 少量 E2E。
@Testcontainers
class OrderRepoIT {
@Container static MySQLContainer<?> db = new MySQLContainer<>("mysql:8.4");
// Boot 3.1+ 可用 @ServiceConnection 自动注入
}
@Test
void domain_should_not_depend_on_adapters() {
classes().that().resideInAPackage("..domain..")
.should().onlyDependOnClassesThat()
.resideInAnyPackage("..domain..", "java..", "jakarta..")
.check(importedClasses);
}
追问 1:全部用 Testcontainers 行吗?不行。过慢且脆;只覆盖「与真实协议/SQL 方言强相关」的路径。
追问 2:契约测和 API 文档谁为准?契约以可执行交互为准;OpenAPI 是文档。理想态由契约/代码生成文档。
追问 3:ArchUnit 能否替代 Code Review?不能。它锁可编码规则;业务语义与取舍仍靠人。
- CI 无 Docker:Testcontainers 全挂,需 DinD/远程 Docker。
- 契约只生成不验证:提供者从不跑验证任务,形同虚设。
- ArchUnit 规则过宽:允许「..」过大,等于没守护。
- 用 E2E 冒充集成测:反馈链路变小时级,团队放弃维护。
多 PropertySource 叠加时,同名键冲突是高频翻车点。桥接目标:能用 Actuator/env 定位最终值与来源,并写成团队约定表,而不是背诵优先级口诀。
Spring Environment 按 PropertySource 顺序覆盖;命令行 / 系统属性 / 环境变量 / 外部配置 / application-{{profile}} / application 是常见骨架,配置中心桥接插入位置以实际实现为准。@ConfigurationProperties 绑定的是最终 Environment,不是某一个 yml 文件。
排查顺序:
- 列出怀疑键(含 relaxed binding 变体:foo.bar / FOO_BAR / foo-bar)。
- 打开
/actuator/env(或 configprops)看该键的 property source 列表与胜出值。 - 核对当前 active profiles、命令行、容器 env、配置中心 DataId。
- 改完后再次核对「来源」是否变成预期源,而不是只看业务行为。
- 把高频冲突键写入团队约定表(谁有权覆盖、灰度怎么推)。
核对表:
- 键名三种写法是否被误当成两个键。
- profile 专属文件是否意外未激活。
- 配置中心推送滞后 / 本地快照是否仍是旧值。
- RefreshScope / rebind 是否覆盖到目标 Bean。
常用口诀仅作起点:命令行 > 系统属性 > 环境变量 > 外部配置 > profile 文件 > 默认 application;以 env 面板为准。
失败模式:以为 yml 胜出实际被环境变量覆盖;本地与生产 profile 不一致;刷新后仍读缓存旧值。回滚点:恢复上一版配置快照并确认 env 来源回到已知好版本。
同名键冲突时优先查「来源优先级表」而非猜:命令行 > 环境变量 > 外部配置 > application-{profile} > application。多文档合并后用 Environment 或 actuator/env(受权)核对最终值。
值班最短路径:①复现同名键 ②列出所有 PropertySource ③确认 profile 与外部配置中心是否覆盖 ④改完做一次只读校验再放量。
量化:以 /actuator/env 的 propertySources 顺序为准,而非背优先级口诀;同名键三种写法(foo.bar / FOO_BAR / foo-bar)在 relaxed binding 下会互相覆盖,核对时须同时列出。演进:把高频冲突键沉淀为团队约定表(谁有权覆盖、灰度路径、回滚版本),并在 CI 增加一步「发布 profile 与生产 profile 一致性」检查,防止环境漂移。