Spring Boot 4 系列收官:2026 年 Java 后端开发者必须掌握的 10 大能力(附 20 道面试题 + 8 周学习路径)

16 阅读18分钟

本文是 Spring Boot 4 系列第 20 篇(收官篇) | 从第一篇《11 项重大变更》到这里,前 19 篇做的是同一件事:把 Boot 4 的某个变化拆到源码级,说清它是什么、为什么这么设计、怎么用。这一篇不再引入新知识点,做三件收尾的事:预计阅读 15 分钟,适合收藏后按周执行。

  1. 把 20 篇压成一张知识地图;
  2. 提炼出 10 项能力——不是 10 个 API,而是 10 种遇到问题时会想起"Spring 4 里应该这么办"的反应;
  3. 给一份面试题 + 学习路径,让这个系列可以被"用起来",而不只是"读过"。

全文所有结论都指向前面某一篇,不新增未经核实的数字。凡是前面文章里明确标注过"这是条件性结论"或"未在本机复现"的地方,这里保留同样的限定词。

内容速览

  • 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-18gRPC、可观测性、安全、AI
前沿展望19-20AOT / Leyden,以及本篇收官

1.2 一张表看完 19 篇正文

篇主题一句话结论
0111 项重大变更Jakarta EE 11 基线、JSpecify、虚拟线程、Jackson 3 是四条主干
02升级避坑spring-boot-properties-migrator 能扫出配置层的坑,但扫不出代码层的
03全维度性能对比启动优化主要来自"运行时魔法前移",不来自某个单点技巧
04JSpecify 实战"默认非空、只标注例外",与 Kotlin 的可空类型是两种哲学
05虚拟线程落地一行配置背后是四条装配链路,不是只有一个线程池在换
06API 版本控制原生支持四种版本解析策略,不再需要第三方库
07BeanRegistrar@Bean 解决不了的"条件化/批量/时序"注册问题,它来解决
08Jackson 3序列化 API 迁到 tools.jackson,注解仍留在 com.fasterxml.jackson
09容错进 spring-coreorg.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 处
14JDBC 惰性连接收益 = 你的缓存命中型请求占比,不是一个固定百分比(详见第 14 篇)
15gRPC 官方支持服务端 @GrpcService,客户端是 @ImportGrpcClients 而非 @GrpcClient
16OpenTelemetry三层架构打通三大信号;4.1 把 @Async 上下文传播接进自动配置(需 opt-in)
17SSRF 防护过滤器必须在"解析后、连接前";四种客户端适配层机制不同
18Spring AI 2.0工具调用循环从模型内部搬到 Advisor 链,这是 2.0 最大的架构变化
19AOT 与 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 启用
CDSJVM 层-XX:SharedArchiveFile
AOT cacheJVM 层-XX:AOTCache,需 Java 25+
Native ImageJVM 之外产出原生可执行文件
  • 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(不走自动配置),官方做法是手动注册 ContextPropagatingTaskDecorator Bean 或自行 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.Builder Bean,没有 ChatClient Bean(且它是 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)

  1. @NullMarked 的语义是什么? 包级/类级"默认非空",只有显式 @Nullable 的才是可空。是"默认安全"而非"逐个标注"。(第 13 篇)

  2. 为什么 Boot 4.1.0 里 @NonNull 只有 1 处? 因为在 @NullMarked 下非空是默认,@NonNull 只在泛型边界等特殊场景才需要。(第 13 篇)

  3. Spring 7 里 org.springframework.lang.Nullable 怎么样了? 被桥接到 JSpecify,存量代码不会立刻失效;但新代码应使用 org.jspecify.annotations.*。(第 13 篇)

虚拟线程

  1. spring.threads.virtual.enabled=true 一共触发几条装配链路? 四条:Web 服务器线程池、applicationTaskExecutor、@Async、Reactor 侧。(第 12 篇)

  2. 虚拟线程的 pinning 问题现在还存在吗? 大幅收敛但没有归零。JDK 24 之后由 synchronized 引起的 pinning 已明显减少,仍有残留场景需要具体分析。(第 12 篇)

  3. 开了虚拟线程,为什么连接池反而成了瓶颈? 因为瓶颈从"线程数量"转移到了"下游资源"。虚拟线程让请求堆积得更快,数据库连接池的并发上限就成了新的天花板。(第 05、12 篇)

AOT / 构建期优化

  1. Spring AOT 和 JVM AOT cache 是一回事吗? 不是。Spring AOT 生成 Java 源码(应用层);AOT cache 是 JVM 层缓存(JEP 483/514)。两者正交且可叠加。(第 19 篇)

  2. 为什么 AOT 模式下 @ConditionalOnProperty 不生效? 因为条件在构建期就已评估并冻结,构建期创建的 bean 在运行期无法关闭。(第 19 篇)

  3. spring.aot.enabled 是谁在读? 不是 Spring Boot 读的——Boot 判断的是 Framework 的 AotDetector.useGeneratedArtifacts()。(第 19 篇)

  4. AOT cache 需要什么 JDK 版本?为什么? Java 25+。注意 JEP 483 本身是 JDK 24,Boot 要求 25 是因为它采用了 JEP 514 的一步式命令(-XX:AOTCacheOutput)。(第 19 篇)

自动配置

  1. 自动配置的候选类从哪来? META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,spring.factories 方式已落幕。(第 11 篇)

  2. 条件评估的两道过滤分别是什么? 第一道不加载字节码就能砍掉一大半候选;第二道才走 Condition 接口做完整评估。(第 11 篇)

  3. "我的自动配置没生效",最快的排查方式是什么? 用 --debug 打印 ConditionEvaluationReport,直接看是哪个条件没通过。(第 11 篇)

扩展点

  1. BeanRegistrar 相比 ImportBeanDefinitionRegistrar 进化在哪? 它是一个函数式接口 + 一个注册表,配合 BeanRegistrationSpec 的链式 API,能更自然地表达"条件化 / 批量 / 时序敏感"的注册需求。(第 07 篇)

API 版本控制

  1. Spring Boot 4 原生支持哪些版本解析策略? 四种(Header / Parameter / Media Type 等),可通过配置选择,不再依赖第三方库。(第 06 篇)

容错

  1. Framework 7 的重试在哪个包?和 Spring Retry 什么关系? 在 org.springframework.core.retry(不是 org.springframework.retry)。它是 spring-core 原生能力,与 Spring Retry 项目是两回事。(第 09 篇)

可观测性

  1. 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 篇)

安全

  1. 为什么用 InetAddressFilter 能防住 DNS Rebinding,JDK 客户端却是例外? Apache/Jetty/Reactor 三个适配层是"在解析结果列表上过滤,过滤后的列表直接用于连接",校验对象与连接对象是同一份数据;JDK 客户端走 ProxySelector 钩子,校验与连接是两次独立解析,属强缓解而非结构性杜绝。(第 17 篇)

  2. InetAddressFilter 的 matches() 返回 true 是放行还是拦截? 放行。它是允许清单语义,写反了会变成全放行——这是安全组件里最典型的实现事故。(第 17 篇)

Spring AI

  1. 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 篇下来最想交付的东西。

系列到此完结。