CHAPTER 06 / SPRING BOOT & MODERN FRAMEWORKS

第 06 篇 · Spring Boot 与现代框架

本篇把 Boot 看成「约定装配 + 生产就绪」:自动配置/Starter → 安全与校验 → AOT/Native → Actuator/配置/停机与云原生测试闭环。

骨架见 M12.06。本篇只补领域细枝,框在 核心方法论 · 开篇。

题目数:15 | 高级 ×9 | 架构 ×5 | 专家 ×1 | 预计复习:170 分钟
核心知识链

Boot 主线是「约定装配与可覆盖 → 安全过滤器链与校验 → 启动编排与外部化配置 → AOT/Native 与可观测/优雅停机 → 测试与替代栈权衡」,目标是可运维默认值。

  1. 自动配置 SPI 与 Starter:EnableAutoConfiguration + Conditional 条件过滤;Starter 用「约定+默认+可覆盖」封装可复用产物。
  2. 启动编排与配置优先级:SpringApplication 事件/Runner/嵌入式容器叠加 refresh;多 PropertySource 叠加冲突用 env 端点定位终值。
  3. Security 与 OAuth2:FilterChainProxy 双阶段认证授权;Security 6 链式 DSL、JWT/Session 选型与微服务令牌传递。
  4. 校验、异常与 Session:Bean Validation 声明式校验;@ControllerAdvice 统一错误出口;Spring Session 外置会话存储。
  5. AOT/Native 与可观测停机:构建期冻结反射与条件;Actuator 端点安全暴露;优雅停机与 K8s 探针/摘流时序对齐。
  6. 测试策略与栈选型: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 摘流时序不一致,就会在「正常发布」里制造故障。
  • 测试防三类腐化:环境漂移、接口背刺、结构腐化分别用容器、契约、架构规则对付。
C06.01
Spring Boot 自动配置的 SPI 机制如何工作?高级P0
本质问题

这道题考察对 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) 导入选择器,这是自动装配的真正引擎。

flowchart TD A["@SpringBootApplication 启动"] --> B["@EnableAutoConfiguration"] B --> C["@Import(AutoConfigurationImportSelector)"] C --> D["selectImports() 触发"] D --> E["getAutoConfigurationEntry()"] E --> F["1、加载候选配置类
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
图 6.1-1 自动配置加载流程:加载候选 → 去重 → 排序 → 条件过滤 → 注册 BeanDefinition

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 仍会生效,需排查全部来源。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.02
Spring Boot 3.x/4.x 的 AOT 与 GraalVM Native Image 如何工作?专家P0
本质问题

这道题考察对 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 把这三点前移:

flowchart LR subgraph Build [构建期 AOT 处理] B1["1、启动 ApplicationContext 到 BeanDefinition 注册后
但不实例化 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
图 6.2-1 AOT 处理流程:构建期冻结条件评估与反射,生成静态代码 + Native 元数据

三大范式对比:

维度 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 构建任务,不能与普通构建混用。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.03
如何设计一个自定义 Spring Boot Starter?高级P0
本质问题

这道题考察把"自动配置 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 为例:

flowchart TD subgraph Starter ["mylib-spring-boot-starter (空 jar)"] S1["pom 依赖:
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
图 6.3-1 双模块拆分:starter 只做依赖聚合,autoconfigure 承载配置逻辑

为何要拆两个模块: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 模块,否则违反官方规范。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.04
Spring Security 的过滤器链与认证授权流程?架构P0
本质问题

这道题考察对 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) 选择第一条匹配的链执行。

flowchart TD A["HTTP 请求"] --> B["Servlet 容器 FilterChain"] B --> C["DelegatingFilterProxy
(桥接 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
图 6.4-1 Spring Security 过滤器链:DelegatingFilterProxy 桥接,FilterChainProxy 按 matches 选链,认证在前授权在后

认证流程(以表单登录为例):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 可被彩虹表破解。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.05
Spring Boot 的启动流程与事件机制是怎样的?架构P1
本质问题

这道题考察对 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() 核心时序):

flowchart TD A["1. SpringApplication 构造
推断 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
图 6.5-1 SpringApplication.run() 七阶段时序:每个阶段发布一个事件,扩展点在事件间隙介入

七个事件的含义与扩展点:

事件 发布时机 此时容器状态 典型扩展点
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 的异步预处理 + 延迟加载。
C06.06
Spring Session 如何实现分布式 Session?高级P1
本质问题

这道题考察对分布式 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,而非容器内存。

sequenceDiagram participant Browser participant Filter as SessionRepositoryFilter participant Wrapper as RequestWrapper participant Repo as SessionRepository(Redis) participant App as 业务Controller Browser->>Filter: 请求 (Cookie: SESSION=xxx) Filter->>Filter: 解析 sessionId 从 Cookie Filter->>Repo: findById(sessionId) Repo-->>Filter: Session(从 Redis Hash 加载) Filter->>Wrapper: 包装 Request,getSession 返回该 Session Wrapper->>App: 业务调用 request.getSession() App->>Wrapper: setAttribute("user", u) Wrapper->>Repo: save(session) (写 Redis,续 TTL) Note over Repo: Redis Key: spring:session:sessions:xxx
Hash 结构, TTL 30min 自动续期 App-->>Filter: 响应 Filter-->>Browser: 响应 (Cookie 回写 sessionId)
图 6.6-1 Spring Session 工作时序:Filter 拦截 → Repository 加载/保存 → Redis Hash 存储 + TTL 续期

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 方案。
C06.07
Spring Boot Actuator 的监控端点与自定义?高级P1
本质问题

这道题考察对 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,应设超时 + 异步。
C06.08
Spring Boot 的配置加载机制与优先级?高级P1
本质问题

这道题考察对 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。规划集群规模时按"客户端数/单节点上限"预留冗余。(以上为业界参考基准,需以自身生产数据替换)

C06.09
Spring Boot 全局异常处理如何设计?高级P1
本质问题

这道题考察对 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,第一个成功处理的生效:

flowchart TD E["Controller 抛出异常"] --> R1["1、ExceptionHandlerExceptionResolver
处理 @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
图 6.9-1 Spring MVC 异常处理链:@ExceptionHandler 优先级最高,匹配后直接处理

@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。
C06.10
Spring Validation 数据校验机制?高级P1
本质问题

这道题考察对声明式数据校验体系的理解。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。
C06.11
Spring Security 6 的安全架构与 OAuth2 实践?架构P0
本质问题

这道题考察对 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() 的资源应改用 WebSecurityCustomizer Bean,否则每个静态请求都进 FilterChainProxy 增加开销。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.12
Spring Boot 优雅停机与 K8s 滚动发布协同(探针/Lifecycle/PreStop)架构P0
本质问题

这道题考察 应用生命周期与编排平台(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 报错。
生产实践

上线门禁:配置绑定校验、健康检查与滚动发布探针。表达提纲:自动配置条件→生效证据→覆盖与排障。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C06.13
云原生 Java 新选择:Quarkus/Micronaut vs Spring Boot 取舍高级P1
本质问题

这道题考察 云原生场景下 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,收益不足以覆盖改造成本。
C06.14
微服务测试策略如何分层:Testcontainers、契约测试与架构守护如何组合?架构P1
本质问题

这道题考察可维护的质量金字塔在云原生下的变形:纯 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 冒充集成测:反馈链路变小时级,团队放弃维护。
C06.15
Boot 配置优先级实战:同名键冲突时怎么查最终生效值?高级P1
本质问题

多 PropertySource 叠加时,同名键冲突是高频翻车点。桥接目标:能用 Actuator/env 定位最终值与来源,并写成团队约定表,而不是背诵优先级口诀。

原理映射

Spring Environment 按 PropertySource 顺序覆盖;命令行 / 系统属性 / 环境变量 / 外部配置 / application-{{profile}} / application 是常见骨架,配置中心桥接插入位置以实际实现为准。@ConfigurationProperties 绑定的是最终 Environment,不是某一个 yml 文件。

落地排查

排查顺序:

  1. 列出怀疑键(含 relaxed binding 变体:foo.bar / FOO_BAR / foo-bar)。
  2. 打开 /actuator/env(或 configprops)看该键的 property source 列表与胜出值。
  3. 核对当前 active profiles、命令行、容器 env、配置中心 DataId。
  4. 改完后再次核对「来源」是否变成预期源,而不是只看业务行为。
  5. 把高频冲突键写入团队约定表(谁有权覆盖、灰度怎么推)。
参数与开关

核对表:

  • 键名三种写法是否被误当成两个键。
  • 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 一致性」检查,防止环境漂移。