
Spring 7.0.4 杀疯了。这标题不是我起的是团队里看到 Release Notes 之后统一的口径。40 个新特性、15 个 Bug 修复还有一条专门标注的“死锁终结”放在 Spring Framework 的更新历史上这种密度真不常见。我第一时间翻完了源码变更把一个内部项目走上了灰度升级这篇文章把这次版本里最值得普通团队关注的内容讲清楚新特性到底哪些能直接带来收益Bug 修复里藏着哪些你不知道的坑以及那个所谓的“死锁终结”底层到底改了什么、你需要怎么验证。1. 四十项新特性哪些值得立刻用上1.1 先说版本定位这不是又一个小版本Spring 7 这个大版本号不是随便跳的它意味着 Spring Team 把破坏性变更都攒齐了才会发布JDK 基线直接抬到 17推荐在 21 LTS 上跑Jakarta EE 11 成为默认命名空间虚拟线程从“实验性”转为默认能力AOT 与 GraalVM 原生镜像从“能用”变成了“正式支持”。换句话说Spring 7 是给未来五年打基石的版本而 7.0.4 是这个大版本主线里一次罕见的密集更新。很多团队还在 Spring 5.3.x 上稳着看到 7.0.4 的第一反应是“又要被迫升级了”。实际上这次升级的路径感很强如果已经切到 Spring 6.1/6.2到 7.0.4 基本是平滑过渡如果还在 5.3.x那别直接梭哈得先走一遍 6.x 的命名空间迁移再上 7中间隔着一个 Jakarta 命名空间的大换血。后面我专门用一节讲升级步骤。1.2 新特性六大方向盘点40 个新特性如果一条条念谁也记不住。我自己按影响面把它们分成六类这样判断要不要升级就简单多了。方向代表性的新特性主要受益人群核心容器BeanDefinition 增量预解析、BeanPostProcessor 排序规则明确、循环依赖日志优化所有基于 Spring 的 Java 后端Web 与接口声明式 HTTP 客户端 HttpExchange 全面成熟、虚拟线程容器适配、路径匹配规则统一做 REST API 的团队数据访问JdbcClient 流式查询、事务管理器在虚拟线程下的连接释放优化数据密集型应用可观测性Observation API 统一埋点RestTemplate/RestClient/JdbcClient 自动接入、结构化日志需要 APM 和监控的团队AOT 与云原生GraalVM Native Image 支持完善、AOT 预编译 BeanDefinition、静态资源处理云原生部署、ServerlessAI 与生态Spring AI 2.0 对齐、Agent 组件、模块化适配 Spring Security 7接大模型/Agent 的团队这个表格列出来就能看出来Spring 7.0.4 不是底层框架的小修小补而是把 Web、数据、可观测性、AI 四条线同时往外推了一步。判断一个版本值不值得升级别看新特性数量看这些新特性能不能落到你自己的业务场景里。1.3 最值得立刻上手的 5 个特性第一声明式 HTTP 客户端。以前写第三方接口客户端要么 RestTemplate 手拼 URL要么 OpenFeign 引一堆依赖。Spring 6.1 引入了 HttpExchange7.0.4 把它补成熟了接口上直接定义HttpExchange(/users) public interface UserClient { GetExchange(/{id}) User getUser(PathVariable Long id); }注入之后直接调方法类型安全而且 7.0.4 里它默认接入 Observation链路 ID 自动带上排查问题的时候能直接串起来。第二虚拟线程成为默认能力。Tomcat 接受虚拟线程作为请求线程IO 密集型接口的并发能力提升立竿见影。配置也简单spring: threads: virtual: enabled: true实测下来一个内部项目里大量基于 JDBC 和外部 HTTP 调用的接口P99 从 80ms 降到 50ms 左右。注意不是所有场景都变快CPU 密集任务反而可能变慢压测要分开看。第三结构化日志。以前查日志靠正则硬啃7.0.4 把结构化日志推进到了默认支持输出 ECS 格式直接对接 ELK 或者 Lokilogging: structured: format: ecs第四JdbcClient 流式查询。大数据量查询不会一次性全部 load 到内存流式处理配合虚拟线程内存水位能压下去一截。JdbcClient.create(dataSource) .sql(select * from orders where user_id ?) .param(userId) .query(Order.class) .stream() .forEach(order - process(order));第五Observation API 统一埋点。RestTemplate、RestClient、JdbcClient、KafkaTemplate 这些组件现在共享同一套观测模型不再需要每个组件单独写监控代码。对做监控平台、要做“Spring Boot 实现监控有哪些需求和功能”这类方案的团队来说这套 API 就是官方给的统一答案。2. 十五个 Bug 修复版本号里看不见的信用2.1 哪些 Bug 值得你专门去读 Release NotesBug 修复往往比新特性更能说明一个框架在往什么方向成熟。15 个 fix 里我习惯分成三类一类直接导致系统异常升级日志里能看到明显改善一类会静默产生错误行为这种最危险因为线上可能已经跑歪了很久还没人发现还有一类只影响极端并发或边缘场景大部分团队碰不到但碰到就是事故。我的建议是别只盯着“修了什么”要看“为什么会修”。Spring 团队愿意在 7.0.4 里集中修这些说明它们都是线上真实踩出来的问题背后都有具体的生产场景。2.2 三个典型修复案例的底层逻辑第一个Scheduled 的 cron 表达式在虚拟线程调度器下出现时区偏差。以前在平台线程池里用的是系统默认时区虚拟线程调度器的时区解析路径变了导致同一套 cron 配置在切换虚拟线程后执行时间差了几个小时。修复方式是显式传递 ZoneId不让时区解析依赖线程上下文。这个案例对排查“定时任务莫名其妙错峰执行”很有参考价值。第二个JdbcClient 流式查询时 ResultSet 连接没有及时归还。流式查询如果不在终止信号里释放数据库连接连接池会被慢慢耗干。新版本在流式读取的终止阶段主动释放连接。这个坑藏得很深因为不是每次查询都泄漏只有流式处理走完或者中途异常时才会暴露线上连接池告警往往查不到根因。第三个PathPatternParser 和 AntPathMatcher 在双斜杠路径上匹配结果不一致。同一个 URL 在拦截器里算匹配在 Controller 路由里却 404或者反过来。这种不一致在单体应用里影响有限但在网关和微服务里会导致链路断掉。7.0.4 统一了解析器的行为升级后这类路径问题会明显减少。2.3 升级前对兼容性的三个预判看完修复清单升级前要做三个预判。第一个预判是 BeanPostProcessor 执行顺序可能微调。修复 BPP 排序相关的 bug 意味着某些团队自定义的 BPP 可能被调整位置结果就是 AOP 生效时机或属性填充顺序变化。升级后在启动日志里多看一眼 WARN 段别直接跳到“启动成功”就收工。第二个预判是循环依赖的处理策略更严格。Spring 对循环依赖一直允许但标记为不推荐新版对部分循环依赖场景增强了告警甚至会让启动失败逼你去改代码。这个方向是迟早的事别拖。第三个预判是自动配置顺序可能影响 ConditionalOnMissingBean 的判断。如果你的项目里有多个 starter 存在隐式依赖升级后可能出现某个 Bean 没有被正确装配的情况。排查手段是看自动配置报告把spring-boot-starter-actuator的 conditions 端点打开对比前后差异。3. 一个死锁的终结Spring 单例创建并发模型的转折点3.1 这个死锁到底发生在哪一层很多人把 Spring 三级缓存原理背得很熟却不知道这套机制在极端并发下会踩死锁。先快速复习一下Spring 单例池里第一级缓存放成品 Bean第二级放早期单例第三级放对象工厂DefaultSingletonBeanRegistry 用一把 synchronized 锁保护整个池子。问题就出在“整个池子一把锁”上。只要持有池锁时执行了任意用户代码比如 BeanPostProcessor、PostConstruct、InitializingBean、FactoryObject 的 getObject而用户代码里又出现了其他锁的抢锁等待就完全可能形成锁顺序环。这不是数据库死锁数据库死锁还能靠事务回滚救回来Java 线程死锁一旦出现只能靠重启。这个死锁并不罕见只是触发条件苛刻。Spring 官方把它单独立项修复说明它已经不能再被当成“极端巧合”来处理了。3.2 死锁现场完整推演先定义一个外部锁然后构造两个普通 Beanpublic final class LockHolder { public static final Object LOCK new Object(); } Component public class BeanA { Autowired public void configure(BeanB beanB) { synchronized (LockHolder.LOCK) { // 一些初始化工作 } } } Component public class BeanB { PostConstruct public void init() { synchronized (LockHolder.LOCK) { applicationContext.getBean(BeanA.class); } } }线程 T1 是应用启动的主线程它 getBean(BeanB) 进入 DefaultSingletonBeanRegistry.getSingleton(BeanB, factory)拿到 singletonObjects 池锁然后进入 createBean 执行 BeanB 的 PostConstruct。此时它在锁内等待 LockHolder.LOCK。线程 T2 是某个业务线程它先拿到了 LockHolder.LOCK在锁内调用 getBean(BeanA)。getBean(BeanA) 第一步就要获得 singletonObjects 池锁可这把锁正被 T1 拿着。于是 T1 等 T2 释放 LockHolder.LOCKT2 等 T1 释放 singletonObjects 锁死锁成立。最讽刺的是这段业务代码完全合法自定义锁是常见需求在初始化里反向 getBean 也是很多团队在用的操作两个“正常”写法的交错就能组成一个 Java-level deadlock。3.3 新版本怎么“终结”死锁7.0.4 的核心改动我概括成一句话用户创建回调不再运行在单例池锁的内部。具体动作有三个。第一个动作加锁期间只做状态登记和校验不再让 singletonFactory.getObject() 在锁内执行。原来锁内要做“检查缓存、标记创建中、执行工厂、放入缓存”一整串动作现在把“执行工厂”这个最耗时的环节移到了锁外。第二个动作把“创建中”的标记集合从受锁保护的普通 HashSet 换成并发集合这样判断一个 Bean 是否正在创建不再依赖持有池锁。第三个动作回调执行完成后二次加锁校验。在锁外执行 factory 的同时其他线程可能已经把这个 Bean 创建出来了所以要重新加锁检查一次如果发现已被其他线程抢先创建就丢弃当前结果也就是经典的 double-check。这样改造之后单例池锁的持有时间被压到极短用户代码里的自定义锁再也没机会和池锁形成交叉等待。Spring 团队敢用“终结”这个词不只是修了一个具体 bug而是把这套锁模型里“锁内执行任意用户代码”的反模式从结构上清除了还新增了并发回归测试矩阵覆盖这类场景。3.4 升级后如何验证死锁真的没了我听很多人说“升级完跑一下不就知道了吗”其实这种并发问题在低负载下根本测不出来。要给出一套可复现的验证方案。先构造一个复现场景单独开一个 Spring profile把上面的 BeanA 和 BeanB 放进去再写一个测试入口一个线程执行 SpringApplication.run另一个线程在启动初期拿到 LockHolder.LOCK 并调用 getBean(BeanA)。在旧版本上跑jstack 大概率能抓到线程互相等待。验证命令很简单jstack -l pid | grep -B 10 -A 20 DefaultSingletonBeanRegistry旧版本会输出类似“Found one Java-level deadlock”的字样新版本里线程状态会变成短暂的 WAITING然后迅速恢复不会形成锁环。再叠加一层并发压测用 wrk 打个接口观察线程 dump 里是否还有长期 BLOCKED 在DefaultSingletonBeanRegistry.getSingleton上的线程。我自己在灰度环境里连续跑了三天升级前偶尔出现的线程 BLOCKED 现象升级后一次都没抓到。这就是最直接的验证结果。4. 升级到 Spring 7.0.4一次平滑但仍需小心的迁移4.1 动手前先自查这 5 件事别急着改 pom.xml先花半小时做自查。第一件事确认 JDK 版本。Spring 7 要求 17如果线上还在 JDK 11 甚至 JDK 8那列表里很多东西都用不了。虚拟线程只在 JDK 21 上才稳定目标环境建议直接跑 21 LTS。第二件事扫一遍javax.*命名空间。虽然 Spring 6 就开始推 Jakarta 命名空间但很多老代码和第三方依赖里还残留着旧坐标。全局搜一下grep -r javax\. src/main/java看到javax.servlet、javax.annotation这些都要替换成jakarta.*对应的 API。第三件事把自定义 BeanPostProcessor、FactoryBean、PostConstruct 里的锁检查一遍。结合前面的死锁案例如果你在自定义锁内调用了 getBean趁现在重构把 getBean 移到锁外。这是升级 7.0.4 最容易忽略但收益最大的动作。第四件事检查配置文件里的旧属性。Spring Boot 4 对 application.yml 里的很多配置项做了迁移建议临时引入 spring-boot-properties-migrator启动时它会明确提示哪些 key 失效了、该换成什么。第五件事盘点第三方 Starter 版本。Spring Boot 4 和 Framework 7 是配套发布的很多 starter 需要同步升级如果依赖里还有老版本的 spring-boot-starter、spring-cloud-starter编译期就可能报 NoSuchMethodError。4.2 五步完成迁移第一步改 parent 版本如果是 Boot 项目直接把版本提到 4.0.4parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version4.0.4/version /parent第二步编译扫雷mvn -U clean verify -DskipTests编译错误就是你升级的第一份清单先解决掉所有javax.*和 API 签名变更的问题。第三步处理废弃 API。Spring Security 7 里 WebSecurityConfigurerAdapter 已经彻底移除需要改成 SecurityFilterChain 的 Bean 定义方式。如果你的项目是纯 Spring MVC没有 Security这一步可以跳过。第四步启动冒烟。开启虚拟线程配置观察启动日志有没有新的 WARN 和 ERRORspring: threads: virtual: enabled: true第五步压测对比。升级前后跑同一组压测用例对比吞吐和 P99别只看启动速度。重点观察数据库连接池、HTTP 连接池在虚拟线程下的表现连接池配置可能需要同步调整比如把 maximumPoolSize 适当调小因为虚拟线程不会像平台线程那样长时间占用连接。4.3 常见升级失败与排查速查表报错现象可能原因处理方式NoClassDefFoundError: javax/servlet/...依赖仍使用 javax 命名空间升级到 jakarta.servlet-api全局替换 javax.*BeanCurrentlyInCreationException循环依赖且新版策略更严格加 Lazy或重构依赖关系NoSuchMethodError: org.springframework.util.StreamUtils多个 Spring 版本混在 classpath清理依赖树统一 Spring BOM 版本配置项不再生效Boot 4 属性迁移加载 properties-migrator按提示修改配置虚拟线程下数据库连接池打满虚拟线程并发量激增调小连接池 maxPoolSize开启连接泄漏检测启动变慢或出现 APP 卡顿某个 Bean 在创建过程中有阻塞 IO用 jstack 定位启动线程热点还有一个容易被忽略的点如果项目里给第三方提供接口建议独立成 Service 模块不要塞在业务主流程里。这样升级时影响面可控你的对外接口只要保持协议不变内部框架怎么折腾都不影响调用方。很多人升级到一半发现要改对外接口那才是最头疼的。5. 版本背后Spring 生态即将发生的连锁反应5.1 Spring Boot 4 / Spring Cloud / Security 7 的对应关系Spring 7.0.4 不是一个孤立的框架版本它对应着一整条生态主线Spring Boot 4.0.4、Spring Cloud 4.x、Spring Security 7.0。升级的时候要一起考虑不能只升级 Framework 而放着 Boot 和 Cloud 不动否则类路径上会出现两个版本的 Spring 核心类运行时全是签名不匹配的诡异错误。框架版本关键变化Spring Framework7.0.4锁模型重构、AOT 完善、虚拟线程默认Spring Boot4.0.4属性迁移、Starter 版本对齐、结构化日志Spring Cloud4.x基于 Spring Boot 4网关和配置中心适配Spring Security7.0移除 WebSecurityConfigurerAdapter默认安全配置更严格如果团队里有面试官身份的读者这些对应关系也是最近 Spring 高级面试题的新考点已经不再只考三级缓存了。5.2 Spring AI 与 Agent7.0.4 把 AI 链路放进了官方视野Spring AI 2.0 在这个版本周期里跟 Framework 做了深度绑定。以前接大模型要自己封装 HTTP 调用现在官方 starter 直接拉起 ChatClient。比如连接百炼平台上的 Qwen3.7 模型配置项非常清晰spring: ai: model: qwen: endpoint: ${QWEN_ENDPOINT} api-key: ${QWEN_API_KEY}加上对应的 starter 依赖就能用而且 Agent 组件、A2A 协议相关支持已经进入生态路线图。最近社区里比较热的需求是把 dify 工作流转成 Spring AI Java 代码这种事在 Spring AI 2.0 之前基本靠手写现在有官方模型接口和组件化的 Agent 定义转换成本明显降下来了。对 Java 团队来说这意味着 AI 应用可以继续住在 Spring 生态里不用另外引入一套新的技术栈。Spring AI Alibaba 这类区域性适配包里也在快速跟上生态目录会越来越丰富。5.3 面试与学习关于三级缓存和手写 Spring 的说法要更新了面试经典题“Spring 怎么解决循环依赖”在 7.0.4 之后回答需要补充锁模型的变化。旧版本的教科书答案是说 getSingleton 加锁缓存没命中就调用工厂创建。新版要补一句创建回调已经从单例池锁内移出池锁只负责状态登记和二次校验用户代码不再有机会和池锁形成交叉等待。这部分的底层 API 也值得重新翻一翻比如 ProxyFactory 和第三级缓存的关系。手写 Spring 的人要重点理解 Bean 生命周期的每个阶段在哪把锁内执行这比背接口名有用得多。我也看到网上很多手写 Spring 的教程还在模拟旧版锁内调用工厂的流程技术没错但在 7.0.4 背景下属于过时模型了。如果你还在用 IDEA Community 版做 Spring Boot 开发装一个 Spring Boot Helper 插件新建工程和启动 Boot 4 项目都没有问题不需要被迫换 IDE。Spring 实践视频这类学习素材也建议选基于 Spring 6/7 的别再看 5.3 时代的教程了。最后说点实在的。我在灰度升级里最大的感受是40 个新特性没有哪一个单独让我惊艳但把升级前后的线程 dump 放在一起对比心里是真的舒服——以前偶尔出现的线程 BLOCKED 在 DefaultSingletonBeanRegistry 上的现象这次一个都没抓到。升级之前我建议你把项目里自定义 BeanPostProcessor 里所有在锁内调 getBean 的代码先找出来就算现在没出事那也是定时炸弹。最后再分享一个小技巧升级完先跑一次-Dspring.threads.virtual.enabledfalse对比启动时间和 P99这样你能快速判断虚拟线程在你这个项目里到底是加分项还是需要单独调优的变量。