👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、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 这种环境里,事情变得复杂:

  1. SIGTERM 信号到达,触发 Graceful Shutdown

  2. 应用开始执行 close() 流程

  3. 与此同时,如果 Graceful Shutdown 超时,kubelet 会发 SIGKILL

  4. 但在 SIGKILL 之前,JVM 自己的 ShutdownHook 也可能被触发

  5. 两条路径竞争同一把锁——死锁

最恶心的是,这个 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

别急着跳。先做调研

大版本跨越,javax → jakarta 迁移成本不低

从 6.x 升级的三个硬性前提:

  1. JDK ≥ 17 ,推荐 21+——Spring 7.0 的最低要求。还在 JDK 8/11 的项目,这一步就是个大工程

  2. 所有依赖完成 Jakarta 迁移 ——javax.servlet → jakarta.servletjavax.persistence → jakarta.persistence,一个不落

  3. 读完官方迁移指南再动手 :https://spring.io/projects/spring-boot——我见过的"直接改 pom 版本号然后花两个月灭火"的案例,都是省了这一步


一句话总结?

7.0.4 不是那种让你兴奋的"大更新"。它做的事情更朴素:修复了你可能正在忍受但没找到原因的问题,加速了你每天都要等的启动过程,减少了你在排查问题时的无效时间。

这种版本不酷,但对生产环境的价值,比任何炫酷的新 API 都大。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

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

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐