
Spring Boot 3.5.9 发布有一阵子了作为3.5.x这条稳定线里的小版本它没有轰轰烈烈的大招但每个补丁都在悄悄解决我们在生产环境里真正头疼的问题。这篇文章我想从工程视角聊聊它的演进以及我们在升级和落地过程中实实在在能拿到手的价值不是舔功能清单是讲怎么用、为什么值得用。如果你手里正维护着一个用 Spring Boot 2.x 写的老系统或者刚把项目迁到 Spring Boot 3 没两年天天被各种依赖兼容问题缠着又或者在纠结“监控到底怎么做”“对外接口该不该拆独立服务”“IDEA 社区版能不能跑 Spring Boot”这类问题那这篇东西应该能帮上忙。我把升级过程、配置变更、监控体系建设、接口拆分思路还有和 Python FastAPI 的选型对比都串在一起讲全部基于 3.5.x 这条线的实际工程场景。1. 3.5.9 在 Spring Boot 演进线上的位置1.1 从 3.x 到 3.5一条清晰的基线收敛Spring Boot 3.x 这条线走到现在你回头看会发现一个很明显的特点它不是靠堆新功能来吸引人的而是在反复打磨地基。3.0 到 3.5 这段时间里整个生态把重心放在了三件事上——Jakarta EE 的彻底切换、Spring Framework 6.x 基座的稳定性、以及 GraalVM 原生镜像和虚拟线程这类新一代运行能力的成熟。3.5.x 的意义在于它把前面几个版本铺开的技术方向收敛成了一套可落地的配置基线。换句话说3.0 时代你还要折腾很多兼容开关到处查“这个配置还能不能用”到了 3.5大部分实验性的东西已经稳定下来了文档、默认值、三方库适配也都跟上了。这一点对工程团队极其重要因为版本升级最怕的不是代码重构而是生态缺位——你升上去了结果有个 Spring 社区之外的中间件还在 2.x 逻辑里打转。3.5.9 作为这条线里靠后的维护版本接手的是一个已经打磨得比较顺的底座。对多数团队来讲不需要纠结“3.5.8 和 3.5.9 到底差在哪几个 commit”而应该把注意力放在整条 3.5.x 线路的策略选择上用小版本迭代持续吸收 bugfix 和补丁同时保持基线依赖的稳定性。1.2 补丁版本不等于没价值很多博客喜欢把“新版本”解读成“新功能发布会”但 Spring Boot 这种大版本里的小补丁版本功能变化往往少得可怜真正值钱的是三个部分CVE 修复、与 Spring Framework 6.2.x 的同步升级、以及第三方依赖的版本对齐。CVE 修复不用多说安全问题永远是第一优先级。 第三方依赖对齐这点很多朋友容易忽略——Spring Boot 的版本线不是孤立存在的它同时约束了几百个依赖的默认版本。 3.5.x 里的每一个小版本都会跟着 Logback、Jackson、Tomcat、Netty 等核心依赖的安全版本做一次整体校准。 你停留在 3.5.0 不升级短期内代码没问题但生态整体已经往前走了这种“静默漂移”才是生产环境最阴的坑。所以我在团队里定过一个很朴素的规矩除非有特殊情况小版本补丁出了就跟大版本升级则要严格做评估。 3.5.9 这样的版本正是“小额跟进”策略里的理想目标。1.3 升级前需要看清的版本矩阵做升级评估第一件事不是翻 Release Notes而是先看版本矩阵。Spring Boot 的每一个大版本都有对应的 Java 版本区间、Spring Framework 版本、以及 Jakarta EE 规范版本。3.5.x 的基础要求很明确Java 17 起跳Spring Framework 6.2.xJakarta EE 10 这代规范。建议你在动手之前把下面这张表拉出来让团队每个成员都过一眼。项目Spring Boot 3.5.x备注Java 版本基线17 起跳21/23 是推荐的运行版本具体以官方文档为准Spring Framework6.2.x这是 3.5 与 3.4 之间最核心的底层差异Jakarta EE10javax.* 到 jakarta.* 的过渡2.x 时代老代码必须重构Tomcat 默认版本10.1.x容器版本跟着 Jakarta 一起换代虚拟线程支持可通过配置开启需要 Java 21 运行时才能发挥完整能力可观测性Micrometer Observation3.x 之后 tracing 和 metrics 走同一条配置路径这张表的价值在于它能快速告诉你“升级到底动到哪些层”。如果你现在的系统是 Spring Boot 2.6.x 甚至 2.3.x那不是一次晋升是连续跨大版本必须按“2.x - 3.0 - 3.5”这种节奏去拆别想着一步到位。2. 核心基座的升级与工程影响2.1 Spring Framework 6.2.x 带来的底层变化Spring Framework 6.2 是 Spring Boot 3.5 的底层基石。它最核心的变化不是 API而是对基础设施抽象的重整。在 6.2 里Spring 加强了对 HTTP 客户端抽象的统一、对响应式能力的梳理还有并发编程模型的调整。从工程视角看最直接的冲击是包路径迁移。你代码里所有的javax.annotation、javax.persistence、javax.validation都要换成jakarta.*这一点写在任何一篇升级文档里却是实际操作里最容易“改漏”的地方。改漏的典型表现是编译能过启动也不报错但跑起来某条链路就挂最坑的是事务注解失效这种问题排查半天才发现是导包导错了。我建议升级之后全局搜一遍import javax一个都不要留。 之前我就遇过团队把javax.annotation.PostConstruct漏在三层业务代码里启动时 Spring 不认这个生命周期注解结果部分初始化逻辑直接跳过了线上出现偶发空指针查了两天才定位到。2.2 Java 版本支持范围与 JDK 选型Spring Boot 3.5.x 官方基线是 Java 172025 年后的补丁版本陆续完善了 Java 24 的运行时支持。 但我要给个偏保守的建议没有特殊诉求的话生产环境优先停在 Java 17 或 Java 21 的长期支持版本上。Java 21 的价值在于虚拟线程 如果你打算体验 Spring Boot 3.5 的虚拟线程能力JDK 21 是性价比最高的选择。 而 Java 17 的好处是生态兼容最稳尤其是一些偏底层的字节码工具、JVM Agent、性能分析器新 JDK 版本上往往跑不转。这里有一个实际经验可以分享JDK 升级和 Spring Boot 升级要拆开做。 先把 Spring Boot 升到 3.5.x 但暂时跑在 JDK 17 上等业务稳定了再单独切 JDK 21。两步混在一起的话出了问题你很难判断是 Spring 行为变了还是 JVM 版本带来的差异。2.3 Jakarta EE 与第三方依赖的版本对齐第三方依赖的版本对齐是升级 3.x 之后每个人都会撞上的墙。最典型的就是 MyBatis。我的一个朋友维护着一个“Spring Boot MyBatis 的跨境商城”项目升级到 Spring Boot 3 以后MyBatis Spring 适配器、分页插件、通用 Mapper 全都得换版本有些草根插件作者更新很慢甚至直接断更。我的建议是升级之前用 Maven 的 dependencyManagement 先把所有核心依赖列一遍对照官方维护的依赖清单逐一确认版本。 那种“本地编译不过再改”的做法在这种大规模迁移里会把自己拖垮。如果你用的是国内常见的一些开源商城源码或脚手架更要小心很多脚手架里的依赖版本是写死的旧版升级 Spring Boot 时不会自动匹配。 最有效的方式是先升级 Spring Boot BOM再让 Maven 重新解析找出所有 conflict挨个处理。3. 值得落地的关键技术价值点3.1 虚拟线程并发模型的工程性价比虚拟线程是 Java 21 推出的特性Spring Boot 3.5 对它的支持趋于成熟。开启方式很简单在配置文件里加上spring.threads.virtual.enabledtrue然后查看日志你会发现 Tomcat 的线程池行为会发生变化。虚拟线程真正解决的场景是“高吞吐、高 IO 等待”的服务。 比如一个接口要调外部 HTTP 接口不动用虚拟线程时每个请求会占一个平台线程平台线程在等待响应期间是不能复用的一旦并发量上去线程池就烧穿了。 换成虚拟线程后等待期间 JVM 会自动把执行上下文切换出去同样一台机器能扛住的并发量明显上升。但注意虚拟线程不是银弹。 如果你的代码存在synchronized块、阻塞式数据库连接池比如默认的 HikariCP 最大连接数就那么几个虚拟线程也救不了你。 我实测下来虚拟线程最主要的收益集中在 IO 密集型场景如果你的瓶颈是在数据库连接池或者纯 CPU 计算上收益会非常有限先不用浪费精力迁。下面是两个常见的配置位置我一般在application.yml里这样写spring: threads: virtual: enabled: true配合虚拟线程我还会顺手放宽 Tomcat 的请求参数server: tomcat: threads: max: 50 accept-count: 1000这里解释一下开启虚拟线程之后Tomcat 的 worker 线程数反而要调小因为阻塞等待这件事交给虚拟线程去做了平台线程只需要负责分配虚拟线程和接收请求。 如果你不调整默认的 200 个线程仍然会带来不必要的内存开销。3.2 监控体系不只是加一个 Admin 面板搜索热词里出现了“Spring Boot 实现监控都有哪些需求和功能”这是个非常接地气的问题。Spring Boot 应用做监控一般分三个层次存活检查、运行指标、链路追踪。存活检查用spring-boot-starter-actuator暴露/actuator/health端点设置好探活路径就行。 运行指标要接 Micrometer把 JVM 内存、GC 次数、线程池状态、HTTP 请求响应时间这些指标推到 Prometheus再用 Grafana 做可视化。 链路追踪在 Spring Boot 3.x 里也变简单了因为 Micrometer Tracing 统一了 API你可以选 Zipkin 或者 OpenTelemetry。很多团队喜欢再加一个 Spring Boot Admin用来做集中式的界面管理。 这个组件分 Server 和 Client社区版完全够用。 我建议定位成“日常开发环境的辅助工具”线上监控的重任最好还是交给 Prometheus Grafana Alertmanager。 Spring Boot Admin 能看线程栈、环境变量、日志级别动态调整但它的自定义告警能力比较弱不如时序数据库那套灵活。拿我的一个实际项目举例监控指标我至少会盯下面几个指标来源说明JVM 堆内存使用率Micrometer JVM 指标判断是否有内存泄漏趋势活动线程数Tomcat 线程池指标核心线程吃满时预警P99 响应时间HTTP 服务指标聚合接口性能的真实数据GC 暂停时间G1 日志/Micrometer容量规划和容器配置调整的依据连接池使用率HikariCP 指标数据库连接耗尽的前兆这里有一点要注意Spring Boot 3.x 的 metrics 端点和 2.x 有细微差异配置management.endpoint.metrics.enabled这类老开关可能要重新校验。 从 3.0 开始配置属性改为management.*前缀很多 2.x 写法都失效了升级后务必打开management.endpoints.web.exposure.include*检查一遍再关闭多余端点。3.3 对外接口的模块化设计单独服务还是内嵌模块热词里有一条“Spring Boot 对外提供的接口给第三方应该放在哪里是单独的服务还是放在对应的业务模块”这道题不少团队都问过。 我的答案很简单看边界不看“物理位置”。如果你的对外接口和内部接口共用一套业务逻辑、共用同一个数据库、共用同一套事务边界那强行拆微服务只会带来一堆分布式事务问题。 复合场景没法绕开分布式事务最终要么上 Seata 这类框架要么你手动做对账补偿工程复杂度立刻上来几个量级。更合理的做法是在单体应用内部拆出独立的 Web 层模块。 比如 Maven 多模块结构第三方接口放在external-api模块里通过一个独立的RestControllerAdvice统一处理错误码内部接口放在internal-api模块里两者中间共享service层但对外不让内部实体直接穿透。只有一种情况我建议直接拆独立服务第三方接口的调用频率极高、对方对可用性要求苛刻、且接口域名需要独立部署多副本的时候。 这时候拆出来可以避免第三方流量把内部核心链路冲垮。 真正的断舍离不是“所有对外接口都独立服务”而是“核心流量和次要流量物理隔离形成一种保护线路”。3.4 Spring Boot 3 与 Python FastAPI 的选型讨论热词里还有一条“后端 Spring Boot 3 和 Python FastAPI”这俩放在一起对比其实答案不在框架本身而在团队和业务。 FastAPI 的优势是开发效率极高、上手门槛低、对异步 IO 的处理非常自然写一个小型服务、AI 应用网关、数据同步脚本FastAPI 半天就能搞定。Spring Boot 3 的优势是生态完备、事务处理成熟、与 Java 企业级技术栈无缝衔接。 一旦业务涉及复杂的银行级事务、多租户账号体系、严谨的角色权限Spring Boot 仍然是更稳的选择。 FastAPI 做这类事情也不是不行但所有组件都要你自己拼质量参差不齐出了问题得自己扛。我见过团队把内部一个报表服务用 FastAPI 写两周上线运行得很好。 也见过团队非要在一个 Spring Boot 多商户商城里嵌一段 Python 算法服务结果两边维护语言都要双倍成本。 选型这事儿尊重业务边界比崇尚技术审美重要得多。4. 从 3.5.9 看工程实践中的真实问题4.1 2.x 升级到 3.x 时的兼容性坑如果你还在 Spring Boot 2.3.x 或 2.6.x想直接跳到 3.5.9那基本等于做一次架构迁移。 除了前面提到的javax换jakarta还有一个高频坑是 Spring Security 的配置写法。 2.x 时代大家都在用WebSecurityConfigurerAdapter这个类在 3.x 直接被删了取而代之的是SecurityFilterChainBean 的声明式写法。 不熟悉 Lambda DSL 的话升级过程会相当难受。另外一个隐蔽问题是配置属性名的大面积调整。 Spring Boot 2.x 的spring.datasource.*在 3.x 仍然可用但像spring.jpa.hibernate.ddl-auto、server.servlet.session.*这一类配置有的改名了有的被移到新命名空间。 最好的排查方式是用官方提供的配置属性迁移工具或者把你线上的配置文件拿到新版本环境里跑一遍看启动报错清单。升级时我一般会保留一个废弃属性检查开关。 在 3.5.x 中可以通过设置让系统在检测到未知或废弃配置属性时直接报警告日志这些日志是很好的线索。 别直接把这些日志屏蔽掉一字一句看一遍能省下后面大量的线上排查时间。4.2 IDEA 社区版没有 Spring Initializr 怎么跑 Spring Boot知道吗热词里有人在问“IntelliJ IDEA 社区版怎么用 Spring Boot”。 社区版确实不带 Spring Initializr 面板但不代表不能开发 Spring Boot 项目。 我团队里不少同事用社区版照样提交代码到仓库构建、调试、跑单元测试都没问题。方法是先去 start.spring.io 网页生成项目骨架选好版本号比如 3.5.9、语言、构建工具、依赖下载解压后用 IDEA 社区版直接打开文件夹作为 Maven 项目。 只要本机装了 JDK 17 和 MavenIDEA 社区版就能识别出项目并自动拉取依赖。 启动类SpringApplication.run可以直接右键运行断点调试也完全可用。社区版唯一缺的或者说用得比较别扭的是没有 Spring 专有的 Bean 注入检查和Autowired的跳转提示。 但这只影响使用体验不影响工程开发。 如果遇到一些“高质量”的开发需求可以在 Maven 里装一个 spring-boot-maven-plugin 的run目标用命令行mvn spring-boot:run配合调试也可以只是没图形界面那么直观。4.3 依赖管理的方式选型parent 还是 BOMSpring Boot 3.5.x 的依赖管理有两种常见姿势一是继承spring-boot-starter-parent二是引入spring-boot-dependenciesBOM。 大多数单模块应用直接继承 parent 最简单配置文件少属性覆盖方便。 多模块项目我推荐 BOM 方式因为这样根 POM 可以不强制继承 Spring Boot 的 parent自由度更高方便对接公司内部的统一父 POM。但要注意如果用了 BOM 方式spring-boot-maven-plugin仍然要单独声明而且版本要跟 Spring Boot 版本对齐。 否则你在 IDEA 里跑mvn spring-boot:run时会发现运行的类路径和依赖版本跟你想象的不一样。我这里有一个多模块项目推荐的基础配置可以直接抄dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version3.5.9/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementdependencyManagement里锁定了所有 starter 和相关生态的版本子模块里引入依赖时就不用写版本号了。 这个做法对控制依赖漂移特别有效CI 构建的可重复性也会大大提升。4.4 常见的启动与运行时问题排查速查有热度关于 Spring Boot 的疑问里启动失败和运行期异常应该占了一半。 我整理了一张速查表都是我在 3.5.x 实测中遇到的高频问题。现象可能原因处理建议启动报ClassNotFoundException: javax.servlet.*还在用老 servlet 依赖或用了 2.x 的插件全局搜javax.servlet改成jakarta.servlet并升级对应依赖接口能启动但事务不生效Transactional导包导成了javax.transaction改注解为jakarta.transaction.Transactional或继续用 Spring 自己的MyBatis 运行时找不到实体映射实体类依赖了错误版本 mybatis starter升级mybatis-spring-boot-starter到适配 Spring Boot 3.5 的版本/actuator/health404未添加 actuator 依赖或端点未开放引入 starter并配置management.endpoints.web.exposure.include连接池耗尽数据库压力不大线程池配置太小或阻塞等待过多用虚拟线程 调大 HikariCP 的 maximum-pool-size 观察开启了虚拟线程但测不出效果业务代码里有大量线程池锁竞争或 synchronized先梳理热点看看是否存在共享锁阻塞这里的每一条我都在真实项目中踩过。 很多问题不是 Spring Boot 本身的质量问题而是迁移时被旧版本习惯带偏了。 升级之后新建一个“醒来先跑冒烟用例”的清单能帮你把这类问题挡在测试环境而不是放任到线上。5. 我在生产环境里切换 3.5.9 后的几点体会5.1 监控与告警配置的细节观察切到 3.5.9 之后第一件事就是把 Prometheus 的采集配置跑通。 你只需要在依赖里加micrometer-registry-prometheus然后配置暴露/actuator/prometheus端点Prometheus 端到端的指标就自动有了。 但默认的指标非常多建议在最终固化前先跑一天看看哪些指标其实你根本用不上通过management.metrics.enable把它们关掉减少存储成本。另外注意Micrometer 从 1.13 开始对某些指标的名字做了调整比如 HTTP 服务端指标从http.server.requests改名成了http.server.requests还是保留旧名具体看你抓到的数据。 如果你的 Grafana 面板是从 2.x 时代继承下来的可能需要在 query 里兼容新旧两种标签。 我建议自制一个简单的 JVM 面板从零开始配置比硬迁老面板快得多。5.2 虚拟线程开启后的真实观测我在一个电商后台服务里开过虚拟线程这个服务主要做商品同步要频繁调第三方接口。 开启虚拟线程后同一个接口的吞吐量大概提升了 60%而 JVM 的线程数从 200 个平台线程变成了少量平台线程 几千个虚拟线程。 监控面板上看到的效果就是线程池不再频繁触发拒绝策略P99 响应时间也稳定下来。但我同时发现一个问题如果业务代码里有比较重的synchronized块虚拟线程反而会拖慢性能。 因为虚拟线程阻塞在锁上时并不会释放底层载体线程的调度权反而容易导致载体线程卡住。 所以开启虚拟线程前先排查一遍代码里是否有大范围的锁竞争如果有先把锁并发度降下来再考虑升级。5.3 几个容易被忽视的配置细节最后分享几个我在升级环境后容易踩的小配置点。第一server.forward-headers-strategy在 3.x 里默认值有调整如果你前面挂了 Nginx 或负载均衡务必配上framework或native否则拿到的客户端 IP 全是代理地址。第二spring.jackson.time-zone在 3.5.x 里建议显式设置。 有的老代码依赖服务器默认时区切到新版本后如果容器时区不是中国标准时间序列化出来的时间会和你想象的不一致。第三spring.main.allow-circular-references这个开关在 3.x 里默认改成了 false。 很多 2.x 时代靠循环依赖“偷偷”工作的代码到 3.5 会直接启动失败。 治本的办法是重构把循环依赖拆开而不是把这个开关重新打开否则你在新版本里埋下了更大的隐患。实际去把循环依赖清掉收获的不只是能升级而是整个 Bean 的创建链路变得清晰可见这对后面加功能、做测试都很有好处。写到这里我能表达的基本都在上面了。 我个人在实际操作中的体会是Spring Boot 3.5.9 不是一个能让你眼前一亮的版本但它是一个值得认真对待的工程基准。 升级过程中遇到的那些问题很大程度上是过去技术债的集中兑现。 如果把时间轴拉长现在付出的一次性迁移成本换来的是一年甚至更长时间内持续稳定的交付效率。 比起追逐新特性不如先把基线钉死在靠谱的版本上然后在这个基础上去谈架构演进才是做后端工程该有的态度。