Spring 7.0.4 杀疯了,40 个新特性、15 个 Bug 修复、1 个死锁终结!
👉 这是一个或许对你有用的社群
🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:
《项目实战(视频)》:从书中学,往事中“练”
《互联网高频面试题》:面朝简历学习,春暖花开
《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题
《精进 Java 学习指南》:系统学习,互联网主流技术栈
《必读 Java 源码专栏》:知其然,知其所以然

👉这是一个或许对你有用的开源项目
国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构
RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRM、AI大模型、IoT物联网等功能:
多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro
微服务:https://gitee.com/zhijiantianya/yudao-cloud
视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本
一个场景
凌晨三点,告警响了。
K8s 集群里,某个核心服务的 Pod 健康检查连续失败,被 kubelet 杀掉重建。新 Pod 拉起来了——但卡住了。进程在,端口没起,日志最后一行停在 Initializing Spring DispatcherServlet。
top 一看,CPU 几乎是 0%。jstack 打线程 dump,发现两个线程互相抱着对方的锁不放:一个是 AbstractApplicationContext.close(),另一个是 JVM 的 ShutdownHook。死锁。经典的死锁。
你重启 Pod,好了。过了两天,又卡了。
这不是假设,这是真实发生在 Spring 7.0.0 ~ 7.0.3 上的问题。 2026 年 2 月发布的 7.0.4,第一件事就是把这个 Bug 钉死在棺材里。
但这篇文章不只是讲一个 Bug 修复。7.0.4 的 changelog 上写着 40+ 新特性、15 个 Bug 修复、18 项文档改进——关键问题是:哪些跟你有关? 我挑出了真正影响日常开发的部分,逐个拆解。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
凶手找到了:#36260
Issue [#36260](javascript:;) 的标题很平淡——"Potential deadlock during context close"。但它的杀伤力一点都不平淡。
根因是这样的: Spring 在处理应用关闭时有两条并行路径。一条是正常的 ConfigurableApplicationContext.close() 流程,会发布 ContextClosedEvent 并销毁 Bean;另一条是 JVM 的 ShutdownHook,在 System.exit 时触发。正常情况下这两条路径不会同时执行。但在 K8s 这种环境里,事情变得复杂:
SIGTERM 信号到达,触发 Graceful Shutdown
应用开始执行
close()流程与此同时,如果 Graceful Shutdown 超时,kubelet 会发 SIGKILL
但在 SIGKILL 之前,JVM 自己的 ShutdownHook 也可能被触发
两条路径竞争同一把锁——死锁
最恶心的是,这个 Bug 是概率性的 。取决于 GC 暂停时间、线程调度顺序、Bean 销毁的耗时。你在本地用 mvn test 跑一万次都没事,上了生产环境隔三差五来一次。
Spring 团队的修复方案相当彻底:重写了 ConfigurableApplicationContext 的关闭状态机,引入了独立的关闭标志位和 CAS 操作,确保 close() 和 ShutdownHook 不会同时进入销毁流程。通俗地说:不管谁先到,后到的那个发现"别人已经在关了",就直接让出。
自检方法 :如果你的 Spring 7.x 项目出现过以下症状,很可能就是这个 Bug——
Pod 启动后卡住不动,但进程没退出
jstack看到close()和ShutdownHook线程互相等待问题只在 K8s/容器环境复现,本地无法触发
发生频率随部署频率正相关(部署越频繁越容易出)
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
顺藤摸瓜:还有哪些坑被填了
既然聊到了 Bug,把其他几个有意思的也一并过掉。
#36293 —— 你可能已经踩了但不知道的性能坑。ConcurrentReferenceHashMap 是 Spring 内部的基石级数据结构,Bean 定义缓存、类型转换缓存、AOP 代理缓存都建在它上面。7.0.3 及之前存在锁竞争问题:高并发时,多个线程同时读写这个 Map 会产生不必要的阻塞。你的应用不会报错,但吞吐量会莫名下降——压测时可能表现正常(并发不够高),但真实流量高峰一来就出问题。这类 Bug 是最难定位的,因为没有任何异常日志,只有 RT 变慢。
#36298 —— 改了 Header 但下游没收到。 你在 HandlerInterceptor 里往 HttpEntity 的 Header 里塞了一个 traceId,但下游服务死活收不到。排查半天发现:HttpEntity 的参数解析时没有同步修改后的 Header。这种 Bug 的特点是"逻辑上完全正确,但框架没传过去",调试起来让人抓狂。
#36266 —— WebSocket 重连失败。StompBrokerRelayMessageHandler 在 Broker 重启后无法自动重连。做在线聊天、实时消息推送的团队,用 STOMP 协议中继到 RabbitMQ/ActiveMQ 的场景会踩到:一旦 MQ 重启,Spring 端的 STOMP 连接就断了且不会恢复,只能重启应用。
#36285 + #36226 两个小修复:消息转换器现在支持 MIME 通配符 */*;Netty 的 HeadersAdapter.remove() 不再返回令人困惑的空列表而是 null。都是"不影响主流程但会让你的边界场景代码行为异常"的改动。
「30 秒变 15 秒」是怎么做到的
聊完 Bug,说说好消息。
Spring 7.0.4 在三个维度同时发力优化性能,且三者叠加后效果是乘法关系 而非加法。官方和社区反馈的启动提速在 30-50% 之间,具体取决于你的 JDK 版本和应用复杂度。
维度一:请求映射的路由计算
每一次 HTTP 请求进来,Spring MVC 都需要根据 URL、HTTP Method、Content-Type 等信息找到对应的 Controller 方法。这需要遍历所有注册的映射规则并做匹配。7.0.4 在这条路径上动了四刀([#36275](javascript:;) ~ [#36279](javascript:;)):
哈希算法换了更高效的实现 —— URL 模式的哈希计算是调用频次最高的操作之一,快 5% 放大到百万 QPS 就是肉眼可见的 RT 下降
Bean 查找路径缩短 ——
HandlerMethod解析时去掉了反射链路上的冗余中间对象单 URL 模式走快速通道 —— 大部分
@RequestMapping其实就一个路径,现在跳过通用的模式匹配逻辑直接命中版本映射解耦 —— 用 API 版本控制的项目不再为版本判断交额外性能税
社区实测:网关类服务 RT 降低约 15-20%。 普通 CRUD 服务感知会弱一些(数据库是瓶颈),但路由规则多、Controller 数量大的项目效果显著。
维度二:注解解析不再做重复功
[#36307](javascript:;)。这个改动的适用面极广。
Spring 里几乎所有"魔法"都建立在注解解析上:@Transactional 要解析事务配置、@Cacheable 要解析缓存策略、@Valid 要解析校验规则、@RequestMapping 要解析路由信息……以前,MethodParameter 和 AnnotatedMethod每次被调用都会重新反射解析一遍方法上的注解 。在注解不多的简单场景里影响有限,但现代 Spring 项目动不动就是自定义注解层层嵌套——
@MyTransactional // 包含 @Transactional
@MyCacheable // 包含 @Cacheable
@MyValidated // 包含 @Validated
public void doSomething() { ... }
每次调用 doSomething(),上面三层注解都要重新解析一遍。7.0.4 之后,第一次解析完就缓存了,后续调用直接走内存 。
维度三:Validation 的反射瘦身
[#36274](javascript:;)。Bean Validation 判定验证组时,每次都要通过 Class.getAnnotations() 读取分组规则。在批量导入(一次验证几千个对象)或者复杂多步表单的场景下,这个反射调用会被放大到可观的量级。7.0.4 精简了这条路径上的反射调用次数。
三个优化合在一起的效果: 启动阶段(大量 Bean 注册和注解扫描)提速 30-50%,运行时请求处理提速 10-20%。不是理论值——Spring 团队专门用 spring-petclinic 做了 benchmark。
给想给 Spring 提 PR 的同学一个思路: 以上三个优化的 PR 都不超过 200 行代码。核心方法论很简单——拿 JProfiler/async-profiler 跑一遍你的应用,找到 CPU 火焰图里 Spring 框架内部的热点方法,判断是否有缓存空间或算法改进空间,写 JMH benchmark 证明提升,提 PR。我见过好几个开发者靠这个路线拿到了 Spring 的 Contributor 身份,写在简历上还挺加分。
少写代码的 5 个理由
40 多个新特性里,大部分是内部改进,普通开发者感知不到。但有 5 个确实能帮你少写几行代码,或者少踩一个坑。
① 自定义注解不再"断链"
问题: 你写了一个 @LazyService 组合注解(内部标注了 @Lazy),用在类上,期望懒加载——然而 @Lazy 的语义在嵌套层次深了之后会丢失。特别是你在 @LazyService 上面再包一层 @FrameworkComponent 的时候,到第三层 @Lazy 就不认了。
修复后([#36306](javascript:;) / [#36305](javascript:;)):@Lazy 和 @Validated 现在支持无限深度的元注解穿透。
@Lazy
public @interface LazyService {} // 第一层
@LazyService
public @interface SuperLazyService {} // 第二层
@SuperLazyService // 第三层——以前断链,现在 OK
public class OrderService { }
喜欢搞"注解 DSL"的团队,这个修复对你们意义最大。
② JSON 序列化终于能完全自己说了算
以前你想完全接管 HttpMessageConverter,总会被 Spring Boot 的自动配置"偷偷"加回默认的 Jackson 转换器。在金融系统做精度控制、医疗系统做字段脱敏的时候,这种"好心办坏事"的行为很致命。
现在一刀切干净:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.clear(); // 默认的全部滚蛋
converters.add(new PrecisionJsonConverter()); // 只用我的
}
不用担心哪个 @ConditionalOnMissingBean 又把 Jackson 塞回来了。这次是语义层面的官方支持,不是 workaround。
③ 拦截器报错终于报"名字"了
以前:
ERROR DispatcherServlet - HandlerInterceptor threw exception during preHandle
你有 15 个拦截器,请问是哪个?
现在:
ERROR DispatcherServlet - HandlerInterceptor [c.y.auth.TokenInterceptor] threw exception
就这一个改动,值凌晨三点的一碗泡面。 每个被 P0 故障折磨过的人都懂。
④ 给现有方法加重试,不用再"动手术"
Spring Retry 新增 beforeRetry 回调 + TaskCallback/Callable/Runnable 包装器。以前要给一个方法加重试?改签名、加注解、处理状态——改动量跟重构差不多。现在:
RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.build()
.execute(ctx -> {
// 你的业务逻辑,一行不用改
return orderService.createOrder(request);
});
beforeRetry 回调让你在每次重试前做点事——刷新 Token、切换备用地址、打日志,都行。
⑤ 告别 NPE 盲盒
RestClient 新增 requiredBody()——"我声明这个接口必须返回响应体":
// 以前:body() 返回 null → 下游 NPE → 半夜 oncall
String data = restClient.get().uri("/api/data").retrieve().body(String.class);
// 现在:没响应体就地爆炸,异常信息明确
String data = restClient.get().uri("/api/data").retrieve().requiredBody(String.class);
Fail-fast 原则的教科书式实现。null 这个东西,要么在源头拦住,要么就一路传染到整个调用链。requiredBody() 选了前者。
完整的 40+ 新特性:
https://github.com/spring-projects/spring-framework/releases/tag/v7.0.4
JDK 版本决定你能吃到多少红利
刚才说的「启动提速 30-50%」有个前提没展开——你的 JDK 是几?
7.0.4 同步升级了 Reactor 到 2025.0.3。这个版本的 Reactor 针对 JDK 25 的虚拟线程做了深度调度优化。而 Spring 7.0 本身的多线程异步启动机制也依赖虚拟线程。所以:
你的 JDK | 启动提速幅度 | 原因 |
|---|---|---|
JDK 17 | ~10-15% | 只吃到路由优化和注解缓存的收益,虚拟线程没有 |
JDK 21 | ~25-35% | 虚拟线程可用,但调度器不是最新优化版 |
JDK 25 | ~40-50% | Reactor 2025.0.3 + Spring 7.0 异步启动全部拉满 |
其他依赖升级:Micrometer 1.6.3(监控指标更全)、ASM 9.9.1(支持 JDK 25 字节码)、Apache POI 5.5(大文件导出性能优化)。对大多数人来说影响不大,但如果你做 Excel 报表导出,POI 5.5 的内存优化值得关注。
升级决策:一张表说清楚
你的现状 | 建议 | 风险 |
|---|---|---|
| Spring 7.x + K8s | 马上升,今天就升 | 几乎为零。死锁修复 + 启动提速是刚需 |
| Spring 7.x + 传统部署 | 下个迭代升 | 零风险,没有破坏性变更 |
| Spring Boot 4.0 | 等 Boot 4.0.4 发布后一起升 | Boot 会集成 7.0.4,不用单独操作 |
| 还在 Spring 6.x / Boot 3.x | 别急着跳。先做调研 | 大版本跨越, |
从 6.x 升级的三个硬性前提:
JDK ≥ 17 ,推荐 21+——Spring 7.0 的最低要求。还在 JDK 8/11 的项目,这一步就是个大工程
所有依赖完成 Jakarta 迁移 ——
javax.servlet→jakarta.servlet,javax.persistence→jakarta.persistence,一个不落读完官方迁移指南再动手 :
https://spring.io/projects/spring-boot——我见过的"直接改 pom 版本号然后花两个月灭火"的案例,都是省了这一步
一句话总结?
7.0.4 不是那种让你兴奋的"大更新"。它做的事情更朴素:修复了你可能正在忍受但没找到原因的问题,加速了你每天都要等的启动过程,减少了你在排查问题时的无效时间。
这种版本不酷,但对生产环境的价值,比任何炫酷的新 API 都大。
欢迎加入我的知识星球,全面提升技术能力。
👉 加入方式,“长按”或“扫描”下方二维码噢:

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。





文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
更多推荐
所有评论(0)