本文是 Spring Boot 4 系列第 20 篇(收官篇) | 从第一篇《11 项重大变更》到这里,前 19 篇做的是同一件事:把 Boot 4 的某个变化拆到源码级,说清它是什么、为什么这么设计、怎么用。这一篇不再引入新知识点,做三件收尾的事:预计阅读 15 分钟,适合收藏后按周执行。
- 把 20 篇压成一张知识地图;
- 提炼出 10 项能力——不是 10 个 API,而是 10 种遇到问题时会想起"Spring 4 里应该这么办"的反应;
- 给一份面试题 + 学习路径,让这个系列可以被"用起来",而不只是"读过"。
全文所有结论都指向前面某一篇,不新增未经核实的数字。凡是前面文章里明确标注过"这是条件性结论"或"未在本机复现"的地方,这里保留同样的限定词。
内容速览
- 20 篇总览:五个阶段 + 一张 19 篇正文的一句话结论表
- 10 大能力:空安全类型化、虚拟线程思维、出站安全边界、四种构建期优化、API 版本设计、框架自带容错、跨线程可观测、gRPC 一等公民、AI 集成治理、"框架替你做了什么"
- 20 道面试高频题,按主题分组,每题附简短答案与出处篇目
- 8 周学习路径:按"先建立判断力、再补细节"排序,每周带动手任务和产出
- 一条贯穿 20 篇的暗线:运行期魔法前移到构建期
- 三处数据限定声明(80% 连接占用、AOT 实测数字适用范围、841 个包的统计口径)
一、先看地图:20 篇讲了什么
1.1 五个阶段
| 阶段 | 篇目 | 定位 |
|---|---|---|
| 入门导览 | 01-03 | 建立"Boot 4 到底改了什么"的整体认知 |
| 核心特性 | 04-09 | 逐个拆解标志性新特性 |
| 源码剖析 | 10-14 | 启动流程、自动配置、虚拟线程、空安全、JDBC |
| 实战迁移 | 15-18 | gRPC、可观测性、安全、AI |
| 前沿展望 | 19-20 | AOT / Leyden,以及本篇收官 |
1.2 一张表看完 19 篇正文
| 篇 | 主题 | 一句话结论 |
|---|---|---|
| 01 | 11 项重大变更 | Jakarta EE 11 基线、JSpecify、虚拟线程、Jackson 3 是四条主干 |
| 02 | 升级避坑 | spring-boot-properties-migrator 能扫出配置层的坑,但扫不出代码层的 |
| 03 | 全维度性能对比 | 启动优化主要来自"运行时魔法前移",不来自某个单点技巧 |
| 04 | JSpecify 实战 | "默认非空、只标注例外",与 Kotlin 的可空类型是两种哲学 |
| 05 | 虚拟线程落地 | 一行配置背后是四条装配链路,不是只有一个线程池在换 |
| 06 | API 版本控制 | 原生支持四种版本解析策略,不再需要第三方库 |
| 07 | BeanRegistrar | @Bean 解决不了的"条件化/批量/时序"注册问题,它来解决 |
| 08 | Jackson 3 | 序列化 API 迁到 tools.jackson,注解仍留在 com.fasterxml.jackson |
| 09 | 容错进 spring-core | org.springframework.core.retry 取代 spring-retry 的定位 |
| 10 | 启动流程源码 | run() 的 12 个阶段,以及 Boot 4 在其中的新变化 |
| 11 | 自动配置源码 | 两道过滤 + 三步排序,AutoConfiguration.imports 取代 spring.factories |
| 12 | 虚拟线程源码 | Tomcat 线程池、applicationTaskExecutor、@Async、Reactor 四条链路 |
| 13 | 空安全源码 | 4.1.0 里 @NullMarked 覆盖 841 个包(全部是 package-info.java),@Nullable 1986 处,@NonNull 仅 1 处 |
| 14 | JDBC 惰性连接 | 收益 = 你的缓存命中型请求占比,不是一个固定百分比(详见第 14 篇) |
| 15 | gRPC 官方支持 | 服务端 @GrpcService,客户端是 @ImportGrpcClients 而非 @GrpcClient |
| 16 | OpenTelemetry | 三层架构打通三大信号;4.1 把 @Async 上下文传播接进自动配置(需 opt-in) |
| 17 | SSRF 防护 | 过滤器必须在"解析后、连接前";四种客户端适配层机制不同 |
| 18 | Spring AI 2.0 | 工具调用循环从模型内部搬到 Advisor 链,这是 2.0 最大的架构变化 |
| 19 | AOT 与 Leyden | "AOT" 有四个不同含义;本机实测主要收益来自 JVM 层而非 Spring AOT |
二、10 大能力
19 篇正文里可迁移的东西,收敛成 10 项能力。每项都给出:为什么它重要 → 关键判断点 → 出自哪篇。
能力 1:用类型系统表达空值契约
为什么重要:NullPointerException 是 Java 里最贵的异常——它出现在最远离根因的地方。
关键判断点:
@NullMarked是包级默认:标了它,包里所有类型默认非空,只有显式@Nullable的才是可空。这是一种"默认安全"的设计。- 因此
@NonNull在 Boot 4.1.0 里只有 1 处——它的用途是泛型边界之类的特殊场景,不是日常标注。如果你在代码里到处写@NonNull,说明理解反了。 - 迁移时:
org.springframework.lang.Nullable已被桥接到 JSpecify,旧代码不会立刻报错,但新代码应该用org.jspecify.annotations.*。
出自:第 04、13 篇。
能力 2:用虚拟线程替代"池化 + 调参"的思维
为什么重要:过去十年 Java 后端的并发调优,很大一部分是在"线程池大小该设多少"上打转。虚拟线程改变的是这个问题的前提。
关键判断点:
- 一行
spring.threads.virtual.enabled=true触发的是四条链路(Web 服务器线程池、applicationTaskExecutor、@Async、Reactor 侧),不是单点替换。排查问题时要知道自己在哪条链路上。 - pinning 不是消失了,是变少了。JDK 24 之后
synchronized导致的 pinning 已大幅收敛,但没有归零——仍然存在的场景要具体分析。 - 虚拟线程不是"无代价":它把瓶颈从线程池转移到了下游资源(最典型的是数据库连接池)。
出自:第 05、12 篇。
能力 3:把"出站请求"当成一等安全边界
为什么重要:服务端替用户发请求(URL 预览、webhook、回调、AI 拉取),等于把你的网络身份借给攻击者。这是 SSRF,常年位居 OWASP Top 10。
关键判断点:
- 校验必须在"DNS 解析之后、建立连接之前"。任何在 URL 字符串上做的检查都会漏——
127.1、2130706433、::ffff:127.0.0.1在 JVM 里都等价于127.0.0.1。 - 用
InetAddressFilter时,语义是"匹配 = 放行"(允许清单),写反了会变成全放行。 - 四种客户端适配层的机制不同:Apache HC5 用
DnsResolver、Jetty 用SocketAddressResolver、Reactor Netty 用ResolvedAddressSelector——这三者是"在解析结果列表上过滤,过滤后的列表直接用于连接";而 JDK 客户端走ProxySelector钩子,是"校验与连接分离"的,理论上存在 TOCTOU 窗口,属于强缓解而非结构性杜绝。 - DNS Rebinding 在三个客户端上是结构性免疫的,在 JDK 客户端上依赖 DNS 缓存一致性。
出自:第 17 篇。
能力 4:区分四种"构建期优化"
为什么重要:这是本系列最容易被混淆的概念集群。搞混了就会得出"开了 AOT 就能快 10 倍"这类错误结论。
关键判断点:
| 名字 | 层次 | 生效方式 |
|---|---|---|
| Spring AOT | 应用层 | 构建期生成 Java 源码,运行期用 -Dspring.aot.enabled=true 启用 |
| CDS | JVM 层 | -XX:SharedArchiveFile |
| AOT cache | JVM 层 | -XX:AOTCache,需 Java 25+ |
| Native Image | JVM 之外 | 产出原生可执行文件 |
- Spring AOT 从 Boot 3.0 就有,不是 4.x 新特性(
SpringApplicationAotProcessor的@since 3.0.0)。 spring.aot.enabled不是 Boot 读的属性——Boot 判断的是 Framework 的AotDetector.useGeneratedArtifacts()。而且它还有个"兄弟"属性spring.aot.processing,是构建期用的,两者别混。- AOT 的精确机制是"换一次遍历":正常
refresh()走到finishBeanFactoryInitialization实例化全部单例;AOT 路径改走preDetermineBeanTypes,遍历同一批 bean 但只推类型。理解了这点,@ConditionalOnProperty为什么失效就自然明白了——条件在构建期就冻结了。 - 本机实测(JDK 21 + Boot 4.1.0,2 个 bean 的极小应用):CDS -49.9%,Spring AOT -4.2%,两者组合 -53.6%。主要收益来自 JVM 层;Spring AOT 的收益与应用规模正相关,在小应用上几乎看不到。Leyden AOT cache 与 native image 未在本机复现,不引用其性能数字。
出自:第 03、10、19 篇。
能力 5:把 API 版本当成设计问题,而不是拦截器问题
为什么重要:API 不可能不变。过去靠网关或第三方库打补丁,现在框架原生支持了。
关键判断点:
- Spring Boot 4 的
@RequestMapping(version = ...)原生支持四种版本解析策略(Header / Parameter / Media Type 等),不同策略对应不同的客户端体验和缓存行为。 - 版本弃用可以表达成响应头,让客户端"被通知"而不是"被中断"。
- 这条能力的关键不是 API 本身,而是"在设计接口时就把版本当成一等公民"——等到要改的时候再加,成本会高一个数量级。
出自:第 06 篇。
能力 6:把重试/容错当成框架自带能力
为什么重要:分布式系统的默认状态是失败。以前这需要额外依赖(spring-retry + Resilience4j)。
关键判断点:
- Framework 7 把重试搬进了
org.springframework.core.retry——注意不是org.springframework.retry(那是 Spring Retry 项目)。 - 核心 API 是
RetryPolicy与RetryTemplate,RetryPolicy.builder()提供maxRetries/delay/multiplier/maxDelay/jitter/includes/excludes。 - 注意
maxRetries的语义是"重试次数"而非"总尝试次数",且指数退避的默认参数可能很激进——接第三方 API 时要按 SLA 显式收敛(第 18 篇里 Spring AI 的默认值是max-attempts=10+multiplier=5,就是个典型例子)。
出自:第 09 篇(并在第 18 篇得到印证)。
能力 7:可观测性要"跨线程"思考
为什么重要:链路追踪最容易断的地方,不是网络调用,而是线程边界。异步一开,trace 就断。
关键判断点:
- 三层架构:Observation API → 桥接层 → 具体实现(OTLP / Prometheus / ...)。
- Boot 4.1 的重头戏是
@Async的上下文传播:Boot 把 Framework 已有的ContextPropagatingTaskDecorator(@since 6.1)接进了自动配置的applicationTaskExecutor。注意 4.1 的新增点是"接线",不是那个 decorator 类本身。 - 它是显式 opt-in,不是默认开启:需要
spring.task.execution.propagate-context=true,且带@ConditionalOnClass(ContextSnapshot.class)条件。忘了这行配置,@Async里的 span 依然是孤儿——这是最容易踩的一处。 - 如果你自己定义
AsyncTaskExecutor(不走自动配置),官方做法是手动注册ContextPropagatingTaskDecoratorBean 或自行setTaskDecorator(...)。 - 响应式侧另有开关:
spring.reactor.context-propagation,默认LIMITED(只在tap/handle算子上生效),要全算子透传需显式设为auto。
出自:第 16 篇。
能力 8:gRPC 是一等公民
为什么重要:社区 Starter 长期承担了官方缺失的角色,现在官方补齐了,依赖和配置方式都变了。
关键判断点:
- 服务端用
@GrpcService,客户端用@ImportGrpcClients——注意不是社区方案里的@GrpcClient。这是从社区 Starter 迁移时最容易踩的一处。 - 测试支持走 In-Process Transport,可以不起真实端口就跑集成测试。
- 官方方案与社区方案在功能覆盖上仍有差异,选型要看具体需求(第 15 篇有一张对比表)。
出自:第 15 篇。
能力 9:把 AI 集成当成"又一个客户端"来治理
为什么重要:2026 年接 LLM 已经是常规需求,但它带来的是一套新的运行时模型,不能只当成"HTTP 调用"。
关键判断点:
- Spring AI 2.0 最大的架构变化:工具调用循环从
ChatModel内部搬到了 Advisor 链上。 收益是"可拦截 = 可治理"——工具调用次数上限这类护栏,只有循环在外面才做得出来。 - 只有
ChatClient.BuilderBean,没有ChatClientBean(且它是 prototype 作用域的);@Tool注解的 Bean 不会被自动扫描,必须显式暴露ToolCallbackProvider。这两条是升级时的高频坑。 - 安全边界要重新画:模型 starter 底层走的是厂商官方 SDK(OpenAI 用 OkHttp),不是 Boot 的
ClientHttpRequestFactoryBuilder——所以第 17 篇那套InetAddressFilter管不到 LLM 流量。两套出站链路,两套防护手段。
出自:第 18 篇。
能力 10:理解"框架替你做了什么"
为什么重要:这是本系列贯穿始终的一条暗线。Boot 的卖点是"零配置",但能 debug 零配置的人才是不可替代的。
关键判断点:
- 自动配置的候选类从哪来:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(spring.factories已落幕)。 - 两道过滤:第一道不加载字节码就砍掉一大半,第二道才走
Condition接口做完整评估;之后再经AutoConfigurationSorter三步排序。 - 排错利器是
ConditionEvaluationReport,--debug就能打出来——"为什么我的自动配置没生效"绝大多数情况可以直接从这里读答案。 - 扩展点的代际演进:
@Bean→ImportBeanDefinitionRegistrar→BeanRegistrar。当遇到"条件化 / 批量 / 时序敏感"的注册需求时,BeanRegistrar是新的正确答案。 - Jackson 3 的迁移策略:序列化 API 迁到
tools.jackson,注解留在com.fasterxml.jackson——所以大量存量代码不用改。这是一种"降低迁移成本"的刻意设计。
出自:第 07、08、10、11 篇。
三、20 道面试高频题
按主题分组。每题给简短答案 + 指向的篇目,方便回去看细节。
空安全(JSpecify)
-
@NullMarked的语义是什么? 包级/类级"默认非空",只有显式@Nullable的才是可空。是"默认安全"而非"逐个标注"。(第 13 篇) -
为什么 Boot 4.1.0 里
@NonNull只有 1 处? 因为在@NullMarked下非空是默认,@NonNull只在泛型边界等特殊场景才需要。(第 13 篇) -
Spring 7 里
org.springframework.lang.Nullable怎么样了? 被桥接到 JSpecify,存量代码不会立刻失效;但新代码应使用org.jspecify.annotations.*。(第 13 篇)
虚拟线程
-
spring.threads.virtual.enabled=true一共触发几条装配链路? 四条:Web 服务器线程池、applicationTaskExecutor、@Async、Reactor 侧。(第 12 篇) -
虚拟线程的 pinning 问题现在还存在吗? 大幅收敛但没有归零。JDK 24 之后由
synchronized引起的 pinning 已明显减少,仍有残留场景需要具体分析。(第 12 篇) -
开了虚拟线程,为什么连接池反而成了瓶颈? 因为瓶颈从"线程数量"转移到了"下游资源"。虚拟线程让请求堆积得更快,数据库连接池的并发上限就成了新的天花板。(第 05、12 篇)
AOT / 构建期优化
-
Spring AOT 和 JVM AOT cache 是一回事吗? 不是。Spring AOT 生成 Java 源码(应用层);AOT cache 是 JVM 层缓存(JEP 483/514)。两者正交且可叠加。(第 19 篇)
-
为什么 AOT 模式下
@ConditionalOnProperty不生效? 因为条件在构建期就已评估并冻结,构建期创建的 bean 在运行期无法关闭。(第 19 篇) -
spring.aot.enabled是谁在读? 不是 Spring Boot 读的——Boot 判断的是 Framework 的AotDetector.useGeneratedArtifacts()。(第 19 篇) -
AOT cache 需要什么 JDK 版本?为什么? Java 25+。注意 JEP 483 本身是 JDK 24,Boot 要求 25 是因为它采用了 JEP 514 的一步式命令(
-XX:AOTCacheOutput)。(第 19 篇)
自动配置
-
自动配置的候选类从哪来?
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,spring.factories方式已落幕。(第 11 篇) -
条件评估的两道过滤分别是什么? 第一道不加载字节码就能砍掉一大半候选;第二道才走
Condition接口做完整评估。(第 11 篇) -
"我的自动配置没生效",最快的排查方式是什么? 用
--debug打印ConditionEvaluationReport,直接看是哪个条件没通过。(第 11 篇)
扩展点
BeanRegistrar相比ImportBeanDefinitionRegistrar进化在哪? 它是一个函数式接口 + 一个注册表,配合BeanRegistrationSpec的链式 API,能更自然地表达"条件化 / 批量 / 时序敏感"的注册需求。(第 07 篇)
API 版本控制
- Spring Boot 4 原生支持哪些版本解析策略? 四种(Header / Parameter / Media Type 等),可通过配置选择,不再依赖第三方库。(第 06 篇)
容错
- Framework 7 的重试在哪个包?和 Spring Retry 什么关系?
在
org.springframework.core.retry(不是org.springframework.retry)。它是 spring-core 原生能力,与 Spring Retry 项目是两回事。(第 09 篇)
可观测性
- Boot 4.1 里
@Async的 trace 上下文是怎么传播的? Boot 把 Framework 的ContextPropagatingTaskDecorator接进了自动配置的applicationTaskExecutor;但它是显式 opt-in——必须设spring.task.execution.propagate-context=true,否则@Async里仍是孤儿 span。若自己定义AsyncTaskExecutor,需手动注册该 decorator。响应式侧另由spring.reactor.context-propagation控制(默认LIMITED)。(第 16 篇)
安全
-
为什么用
InetAddressFilter能防住 DNS Rebinding,JDK 客户端却是例外? Apache/Jetty/Reactor 三个适配层是"在解析结果列表上过滤,过滤后的列表直接用于连接",校验对象与连接对象是同一份数据;JDK 客户端走ProxySelector钩子,校验与连接是两次独立解析,属强缓解而非结构性杜绝。(第 17 篇) -
InetAddressFilter的matches()返回 true 是放行还是拦截? 放行。它是允许清单语义,写反了会变成全放行——这是安全组件里最典型的实现事故。(第 17 篇)
Spring AI
- Spring AI 2.0 里工具调用循环在哪里执行?
在 Advisor 链上(
ToolCallingAdvisor),不再在ChatModel内部。所以OpenAiChatModel.Builder.toolCallingManager(...)被标记为废弃。(第 18 篇)
四、8 周学习路径
给已经会 Spring Boot 3、想系统掌握 Boot 4 的人。建议按"先建立判断力、再补细节"的顺序,而不是按篇号顺序刷。
第 1 周:先建立"变了什么"的整体框架
- 读第 01、02 篇,跑一遍
spring-boot-properties-migrator,把自己的项目扫一遍。 - 产出:列出你项目里受影响的清单(配置项 / 包名 / 依赖)。
第 2 周:空安全 + 虚拟线程(两个"改思维"的特性)
- 第 04 + 13 篇(空安全)、第 05 + 12 篇(虚拟线程)。
- 动手:给一个模块加
package-info.java+@NullMarked,感受一下编译器提示的变化。 - 产出:能说清"为什么
@NonNull只有 1 处"。
第 3 周:把框架的"自动"拆开看
- 第 10 篇(启动流程)+ 第 11 篇(自动配置)。
- 动手:写一个自定义 AutoConfiguration,用
--debug跟踪条件评估。 - 产出:能独立回答"我的自动配置为什么没生效"。
第 4 周:扩展点与序列化
- 第 07 篇(BeanRegistrar)+ 第 08 篇(Jackson 3)。
- 产出:完成一次 Jackson 3 迁移演练,确认哪些代码不用改。
第 5 周:网络层的三条线
- 第 06 篇(API 版本)+ 第 15 篇(gRPC)+ 第 17 篇(SSRF)。
- 动手:给一个接口加上版本控制;给自己的出站客户端加上
InetAddressFilter。 - 产出:能画出
InetAddressFilter在四种客户端上的不同生效位置。
第 6 周:可观测性与容错
- 第 16 篇(OTel)+ 第 09 篇(容错)。
- 动手:用一条命令拉起 OTLP + Jaeger,记得先加
spring.task.execution.propagate-context=true,再确认@Async的 trace 不断——不加这行的话,你会以为是配置错了,其实是没 opt-in。
第 7 周:数据访问与 AI
- 第 14 篇(JDBC 惰性连接)+ 第 18 篇(Spring AI)。
- 注意:第 14 篇的收益是条件性的(见第 14 篇的量化分析),别当成固定收益。
第 8 周:构建期优化
- 第 03 + 19 篇。
- 动手:对自己的应用做一次 AOT 处理 + CDS,自己测一遍你项目的真实收益——不要照搬任何文章里的百分比,包括本系列。
五、结语:一条贯穿 20 篇的暗线
如果这 20 篇只能留一个印象,我希望是这个:
Spring Boot 4 这一轮演进,有一条清晰的主线——把"运行期魔法"逐步前移到"构建期"。
把它拆开看,处处都是同一件事:
- 自动配置:条件评估可以在构建期完成(第 11 篇);
- AOT:bean definition 在构建期被编译成代码,运行期跳过扫描(第 19 篇);
- 空安全:把 null 的契约从"运行期靠运气"变成"编译期靠类型"(第 13 篇);
- API 版本控制:把版本从"运行时拦截"变成"声明的一部分"(第 06 篇);
- 容错:把重试策略从"业务代码里写循环"变成"声明式配置"(第 09 篇);
- Spring AI:把工具调用从"模型内部不可干预"变成"Advisor 链上可拦截"(第 18 篇)。
而 Project Leyden 是这条主线在 JVM 那一侧的同向运动——从虚拟机往上顶,和框架往下压,在"编译期与运行期之间的那个阶段"交汇(第 19 篇)。
所以回到标题:2026 年真正需要的能力,不是"会用某个新 API",而是能在"这个行为到底是构建期定的还是运行期定的"这个坐标上定位问题。
一旦建立了这个坐标感,你会发现 Boot 4 的那些"限制"(AOT 下 profile 受限、条件冻结、classpath 固定)都不再是"框架的毛病",而是这条主线的必然代价。能分清哪些代价值得付、哪些场景该绕开——这才是 20 篇下来最想交付的东西。
系列到此完结。