Spring Boot 4.0 的里程碑版本一出来,最让我留意的不是 AOT 编译链路,也不是模块化改造,而是默认 JSON 库从 Jackson 2 换成了 Jackson 3。这条新闻的冲击力,对很多 Java 服务端老手来说不亚于版本基线整体上移。原因很简单:Jackson 2 从 2012 年开始统治 JVM 生态十几年,Spring Boot 从 1.x 到 3.x 内置的 JSON 处理几乎都是它。如今 Spring Boot 4.0 直接“全面拥抱 Jackson 3”,意味着所有基于 Spring Boot 的 Web 服务、消息服务、微服务,都要把依附在 Jackson 2 上的序列化逻辑重新捋一遍。
这篇文章不是简单复读升级公告,我会从“为什么非换不可”讲起,把 Jackson 3 相对 Jackson 2 的核心差异掰开来看,然后给出 Spring Boot 3.x 项目升到 4.0 的迁移步骤、真实改造记录、生态兼容性清单,以及升级过程中的高频坑。无论你现在是刚开始评估 Spring Boot 4.0,还是已经升级到一半被 NoClassDefFoundError 卡住,这篇内容都值得你对照着排查。
1. 为什么 Spring Boot 4.0 要在这个时间点换掉 Jackson 2
1.1 Jackson 2 的“长寿”与代价
Jackson 2 可以算是 JVM 世界里生命周期最长的通用 JSON 库之一。它的 2.0 版本发布于 2012 年,此后十几年的时间里,项目坐标始终维持着 com.fasterxml.jackson.core:jackson-databind 这套结构。Spring Boot、Spring Cloud、各种中间件客户端、代码生成工具几乎都把它当成默认依赖。长寿本身不是问题,问题在于长寿意味着过度兼容。为了不破坏已有用户,Jackson 2 在十几年里一直在同一个包路径和同一个 ObjectMapper API 上叠加功能,结果就是核心类越来越重,很多 API 从出现那天起就带着“历史补丁”的标签,想彻底修正又不敢动。这个状态维持到 JDK 17 全面普及、虚拟线程和强封装都被广泛接受之后,就变得十分尴尬:底层 Java 已经迭代了一两代,JSON 库却还在用一套被历史包袱绑住的接口开发新功能。
我在实际升级评估里看到很多项目的问题其实不在功能缺失,而在“功能太散”。想配置一个字段命名策略,能找到七八种途径;想自定义某个类型的序列化,可能要翻好几个模块的源码。Jackson 2 不是不好用,而是太多年累积下来的路径让人容易选错。Jackson 3 的出现,很大程度就是要把这些历史岔路清理掉。
1.2 Jackson 3 到底想解决什么问题
Jackson 3 与其说是 Jackson 2 的一次大版本升级,不如说是同一团队对这套序列化框架做了一次“重新设计”。它把包名从 com.fasterxml.jackson 整体迁移到 tools.jackson,把最低 Java 版本抬到 17,对 databind 层的 API 做了重新编排,模块划分也彻底理清。目标很明确:在 JVM 现代版本上,实现一套更干净、更可组合、更适合框架集成的 JSON 处理核心。
对 Spring Boot 来说,Spring Framework 7 本身也是一次大基线更新,既然要求 Java 17+,那借用 Jackson 3 的新内核来配合 AOT、GraalVM、模块化这些方向,就顺理成章。换句话说,Spring Boot 4.0 换掉 Jackson 2,不只是一个依赖坐标的替换,而是把整套默认序列化栈从“兼容过去”切换到“面向未来”。这也是为什么官方用了“全面拥抱”这种说法,而不是简单地在依赖管理里升级一下版本号。
1.3 这个变化会影响哪些项目
从影响范围看,只要你的项目通过 spring-boot-starter-web 或 spring-boot-starter-json 引入了 JSON 处理能力,升级到 Spring Boot 4.0 之后,默认拿到的 ObjectMapper、JsonNode、注解都会来自 Jackson 3。如果你的代码只是用 @RestController 加实体对象加自动序列化,那影响相对小,因为大部分常用注解和常见用法在形式上仍然保留;但如果你的项目里手写了大量自定义序列化器、自定义 ObjectMapper 配置、深度依赖 JsonNode 操作,或者引用了大量间接编译进 Jackson 2 的第三方库,那升级范围就要重新评估。
判断迁移工作量的第一个指标很简单:你项目里有多少 Java 文件出现了 com.fasterxml.jackson 开头的 import。如果有几十上百处,说明你确实和 Jackson API 深度绑定了,这部分代码需要认真过一遍;如果只是少数配置类和工具类,那主要精力应该放在依赖冲突排查上。
2. Jackson 3 和 Jackson 2 的核心差异对照
2.1 包名与坐标整体迁移
Jackson 3 最直接的变化就是命名空间和 Maven 坐标全部改变。Jackson 2 时代,核心类都在 com.fasterxml.jackson 下,坐标长这样:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>到了 Jackson 3,核心类迁移到 tools.jackson 下,坐标也对应换成:
<dependency> <groupId>tools.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>这不是机械的“换个包名继续用”那么简单,因为包名一变,所有直接引用 Jackson 的 Java 文件都要改 import。IDE 的全局重命名可以处理一部分,但凡是用了反射、SPI 配置、自定义模块注册的地方,都可能出现遗漏。
另一个很容易忽略的点是:Spring Boot 4.0 的 starter 会默认引入 Jackson 3,但部分第三方依赖仍然会在传递依赖里带进 Jackson 2,最终你的 classpath 里出现新旧两套 Jackson 并存。这种并存初期没有任何异常,一旦某个库把 com.fasterxml.jackson.databind.ObjectMapper 类型的对象传给你的 tools.jackson 代码,类型不匹配的坑就来了。表面上看都是 ObjectMapper,实际却不是同一个类,编译器不会提醒你,运行期才会炸。
2.2 ObjectMapper 的构建方式被“拧紧”了
Jackson 2 里最常见的写法是直接 new ObjectMapper(),然后调用 setSerializationInclusion、configure、registerModule 等方法来调整状态。Jackson 3 把 ObjectMapper 的默认构造方式调整为 Builder 模式,更强调不可变配置。也就是说,新版更推荐用类似这样的方式创建:
ObjectMapper mapper = JsonMapper.builder() .enable(SerializationFeature.INDENT_OUTPUT) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .addModule(new JavaTimeModule()) .build();这样创建出来的 ObjectMapper 配置一旦固化,后续通常不能再通过 setXxx 去改写。对于喜欢在 Spring 配置类里反复修改 ObjectMapper 的团队,这一步很容易遇到困惑:为什么之前能 set,现在不行了?
其实这是 Jackson 3 在底层设计上希望业务代码采用“配置优先、实例不可变”的方式,减少共享可变对象带来的并发问题。Spring Boot 4.0 的自动配置核心逻辑也会围绕 Builder 体系展开,如果你想自定义序列化行为,优先考虑直接向 Builder 注入模块,而不是拿着自动配置出来的 ObjectMapper 再去 set。
2.3 序列化器与反序列化器的 API 重新编排
Jackson 3 对 databind 层做了大量拆解,序列化器、反序列化器的接口和注册方式都有变化。常见的 JsonSerializer 仍然存在,但方法签名、可访问的上下文、SimpleModule 的注册方式都可能发生微调。
举一个我实际改造中遇到的情况:旧代码里继承 StdSerializer ,重写 serialize 方法,内部用 SerializerProvider 的公开方法获取全局配置。到了 Jackson 3,如果当前版本里这些 API 被重构,最直接的表现就是编译不通过,IDE 会明确告诉你哪里改了。麻烦的是行为层面的变化,同一份配置在 Jackson 2 和 Jackson 3 下可能序列化出不同结构。例如某些默认特性开关、时间格式处理、null 值处理策略,官方在升级说明里明确要求以新版文档为准,不能拿 Jackson 2 的默认行为去猜测 Jackson 3。
实践里我的建议是:升级后写一个基础的序列化一致性测试,把关键接口的 JSON 输出与升级前对比,差异点逐项确认后再继续推进。很多人升级半天,代码编译也过了,最后是在线上发现老接口返回结构变了,这样的排查成本远高于一开始就做输出对比。
3. 从 Spring Boot 3.x 升级到 4.0:实操步骤拆解
3.1 升级前先做依赖体检
不要一上来就改版本号,先把你项目的依赖树完整拉出来看一遍。Maven 项目用:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.*Gradle 项目用:
gradle dependencies --configuration runtimeClasspath这一步的目的是搞清楚:项目里除了 Spring Boot 自带的 Jackson 2,还有哪些第三方库直接或间接依赖了 Jackson 2。很多开发者在升级后才开始处理依赖冲突,结果问题叠问题,完全分不清是代码问题还是依赖问题。我建议先体检、后动手,把所有 Jackson 2 的来源列成一张清单,升级时逐个排除或替换。
还需要检查第三方组件的最新版本是否已经适配 Jackson 3。这一步可以去各组件官方仓库的 release note 里确认,不要靠猜。常见的情况是:某些组件虽然发布了新版,但内部仍然依赖 Jackson 2,这时候你把 Spring Boot 升到 4.0,它不会自动跟着用 Jackson 3。
3.2 替换依赖坐标与版本管理
完成体检后,最稳妥的升级路径是先把 Spring Boot 父工程版本切换到 4.0.x:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.0</version> <relativePath/> </parent>然后删除 pom 里所有显式声明的 Jackson 2 依赖,让 Spring Boot 4.0 的依赖管理统一接管 Jackson 3 版本。如果你的项目里有单独引入的 jackson-databind、jackson-core、jackson-annotations 等坐标,直接移除即可。对于第三方库传递进来的 Jackson 2 依赖,可以用 dependencyManagement 里的 exclusion,或者等对应库发布适配版本后再统一排除。
需要注意的是,Jackson 3 的一些扩展模块坐标也变了。比如时间模块、Kotlin 模块、参数名称模块,在新版里可能换了 artifactId 或 groupId。升级时不要只盯着 jackson-databind 一个坐标,所有 Jackson 家族成员都要过一遍。
3.3 代码改造的几个关键点
代码改造的第一步是处理 import。全局搜索 com.fasterxml.jackson,替换为 tools.jackson。如果你用的 IDE 支持正则替换,可以用:
com\.fasterxml\.jackson -> tools.jackson但要小心:部分注解包的迁移路径可能不是简单的同路径替换,比如注解源码如果不在同等目录下,你需要额外确认。我的做法是先全局替换一次,再编译看报错,逐个修正 IDE 提示的无法解析符号。
第二步是检查 ObjectMapper 的创建方式。把所有 new ObjectMapper() 改成 builder 模式,把所有 setXxx 配置改成 builder 链式调用。如果遇到自动注入的 ObjectMapper,确认它是 Spring Boot 自动配置提供的 Jackson 3 版本,不要自己手动再 new 一个。
第三步是检查自定义模块和序列化器注册。SimpleModule 的 addSerializer、addDeserializer 用法整体还在,但包路径变了。注册进 ObjectMapper 的方式建议沿用 builder 的 addModule 方法。如果你在代码里直接使用了一些 Jackson 2 内部类,这些类大概率在 Jackson 3 里不存在或已改名,编译期就会暴露出来,耐心逐个处理即可。
3.4 升级后的基础测试与回归
升级替换完成后,不要直接发布。先跑一遍项目的基础单元测试,重点看 Controller 层的 JSON 输出。我习惯写一个简单的接口测试:调用几个典型接口,把响应的 JSON 字符串保存下来,升级前后做字符串对比。这听起来有点笨,但确实能发现很多单元测试覆盖不到的问题,比如时间格式化方式变化、枚举序列化行为变化、null 字段是否输出等。
如果你的项目有前端联调,建议升级后先把接口文档导出和升级前对比一遍。不要相信“默认行为没变”这种假设,Jackson 3 作为一个重写版本,很多细节就是会不一样。早发现早处理,比上线后让前端同事来找你要容易得多。
4. 一次真实改造:某内部接口服务升级记录
4.1 项目情况与依赖底数
我最近接手一个内部接口服务,规模不大,但典型程度很高:依赖 spring-boot-starter-web、参数校验 starter、接口文档生成组件,另外有一堆自制工具类,里面大量使用了 JsonNode 做动态数据解析。代码里 com.fasterxml.jackson 开头的 import 有六十多处,自定义序列化器有四个,还有一个全局 ObjectMapper 配置类。
升级前我拉了一轮依赖树,发现除了 Spring Boot 自带的 Jackson 2,接口文档生成组件还传递依赖了旧版 Jackson 2。这意味着就算我升级了 Spring Boot,那个组件仍然会把 Jackson 2 拉进 classpath。面对这种情况,我的做法是先看该组件有没有适配 Jackson 3 的新版本,有的话直接升级组件;没有的话先记录在案,评估能否降到次要依赖,或者临时排除之后再找替代方案。
4.2 具体改动清单
第一步,把 spring-boot-starter-parent 改到 4.0.x,重新导入依赖。
第二步,删除 pom 里一个显式的 Jackson 2 坐标。这个坐标是早期为了用某个扩展模块手动加的,Spring Boot 4.0 的依赖管理里已经包含 Jackson 3 对应模块,直接删掉即可。
第三步,全局替换 import。把 com.fasterxml.jackson 替换成 tools.jackson 后,编译报错主要集中在三个地方:一个自定义 JsonNamingStrategy、一个自定义 DeserializationProblemHandler、还有几处直接 new ObjectMapper 的工具类。
第四步,调整全局配置类。原来的写法是:
@Configuration public class JacksonConfig { @Bean ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.registerModule(new SimpleModule().addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer())); return mapper; } }改成了:
@Configuration public class JacksonConfig { @Bean ObjectMapper objectMapper() { return JsonMapper.builder() .serializationInclusion(JsonInclude.Include.NON_NULL) .addModule(new SimpleModule().addSerializer(LocalDateTime.class, new CustomLocalDateTimeSerializer())) .build(); } }这里要注意,Jackson 2 里 setSerializationInclusion 在 Jackson 3 的 builder 里可能叫 serializationInclusion,具体方法名以你引入的版本 IDE 提示为准。核心思路是把“创建后修改”改成“创建前配置”。
第五步,给四个自定义序列化器逐个改 import,其中有两个因为父类或接口签名变化需要调整方法实现。这类问题编译期就会拦住,反而是好事。
4.3 改造中踩到的两个坑
第一个坑是接口文档组件。由于该组件仍引用 Jackson 2 并创建自己的 ObjectMapper,升级后接口文档里部分字段的示例数据和实际返回不一致。我花了不少时间调试才发现是两套 Jackson 各管各的。解决办法是等组件适配版发布后升级,暂时在文档输出环节做兼容处理。
第二个坑是全局替换 import 时,有一个工具类里用了 JsonNode 的某个方法,Jackson 2 返回类型和 Jackson 3 不一致,导致下游调用方出现 ClassCastException。这种问题不依赖编译报错,只能在回归测试里发现。我当时立刻把该工具类的单测补齐,专门覆盖空值、嵌套节点、数组节点三种场景,才定位到问题。
这两个坑说明一个道理:升级 Jackson 3 不只是改包名,运行期的行为变化同样要重视,特别是在你深度使用 JsonNode 的场景下。
5. 生态兼容性评估与过渡方案
5.1 你依赖的组件是否已经支持 Jackson 3
Jackson 3 作为一个重写版本,生态支持必然不是一步到位的。哪些组件需要重点确认?我整理了一个常用的排查清单:
| 组件类型 | 常见代表 | 升级时重点确认事项 |
|---|---|---|
| 接口文档 | 各类 OpenAPI/Swagger 生成库 | 是否适配 tools.jackson 包路径,是否能正确解析 Jackson 注解 |
| 代码生成 | MapStruct 等编译期代码生成器 | 是否识别新注解包,生成代码里引用的是哪个 ObjectMapper |
| 数据访问 | MyBatis、JPA 等持久层框架 | 是否有类型处理器直接依赖 Jackson ObjectMapper |
| 消息队列 | Kafka、RabbitMQ 客户端及序列化器 | 消息体 JSON 转换是否走 Spring 自动配置的 ObjectMapper |
| 缓存 | Redis 相关 Starter | 序列化器是否显式 new ObjectMapper |
| 办公/导入导出 | Excel、CSV 处理库 | 是否依赖 Jackson 2 做内部转换 |
这个表不是让你照单全收,而是提示你升级前要有意识地列一份“影响面清单”。如果你发现某个核心组件还没适配 Jackson 3,可以提前准备过渡方案,而不是等升级后被动等上游发布。
5.2 两套 Jackson 并存时的隔离思路
如果你的项目在过渡期不得不同时面对 Jackson 2 和 Jackson 3,最重要的原则就是:不要让业务代码混用两套 API。隔离思路通常有三种:
第一种,依赖排除。对仍传递 Jackson 2 的组件,如果能确定它不用旧 Jackson 处理关键业务,直接排除旧依赖最干净。但风险在于,如果组件内部通过反射加载类,排除后可能运行期异常。
第二种,第三方组件隔离。对于无法排除的组件,看它是否允许自定义 ObjectMapper,尝试把它的 ObjectMapper 实例替换成 Jackson 3 的包装版本。这样至少让组件与主项目保持同一个 JSON 处理上下文。
第三种,延迟升级。如果隔离成本过高,就把该服务的升级计划延后,等生态成熟再动。这不是认怂,而是务实的取舍。我在实际项目里就是这么处理的,先升级不依赖旧组件的服务,把高风险服务放入第二批。
5.3 保守策略:暂时停留在 Spring Boot 3.x 的几种情况
并不是所有项目都适合立刻升级到 Spring Boot 4.0。如果你遇到下面几种情况,我建议先稳住:
- 项目里有大量深度定制 Jackson 2 行为的代码,比如自定义注解解析器、自定义属性命名策略、自定义反序列化拦截器。
- 你依赖的某些核心组件还没有适配 Jackson 3,而且没有可替代方案。
- 项目团队近期有其他紧急交付任务,没有足够时间消化一次底层基础库更换带来的回归风险。
技术栈升级讲究节奏。Spring Boot 4.0 和 Jackson 3 确实是长期正确方向,但对一个正在稳定运行的老项目来说,平稳比新潮更重要。
6. 升级常见问题速查
6.1 升级阶段常见报错对照
| 现象 | 典型报错 | 原因 | 解决思路 |
|---|---|---|---|
| 启动后类找不到 | NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper | 某第三方库仍通过传递依赖引入 Jackson 2,启动时初始化了旧类 | 检查 dependency tree,排除或升级对应库 |
| 编译时包找不到 | package tools.jackson.databind does not exist | Jackson 3 坐标未正确引入,或 IDE 缓存未刷新 | 确认依赖坐标已切换,执行 clean 后重新编译 |
| ObjectMapper 无法 set 配置 | 找不到 setSerializationInclusion 方法 | 还在用 Jackson 2 时代的 setXxx API,Builder 模式下不可用 | 改为 JsonMapper.builder() 链式配置 |
| 自定义序列化器没生效 | 序列化结果和预期不一致 | SimpleModule 注册方式或模块坐标有问题 | 检查 addModule 是否注册成功,模块包名是否来自 Jackson 3 |
| 接口返回结构变化 | 字段顺序、时间格式、null 值处理不同 | Jackson 3 默认行为与 Jackson 2 有差异 | 写接口输出对比测试,逐项确认差异 |
| 两套 Jackson 并存 | ClassCastException 或 NoSuchMethodError | 业务代码和第三方代码使用了不同版本的 ObjectMapper | 统一 classpath,避免两套 API 混用 |
处理这些问题时,不要靠猜。先看报错堆栈里涉及的类是在哪个 jar 包,再看这个 jar 包是从哪个依赖传递进来的。用 IDE 打开 Maven 依赖分析,通常几秒钟就能定位到源头。
6.2 排查思路与预防建议
我总结了一套自己的排查顺序,供你参考:
先查 classpath 上是否存在两个不同的 jackson-databind。这一步可以用 Maven 插件或 Gradle 依赖报告完成。classpath 干净,能避免绝大多数诡异运行期问题。
再查 ObjectMapper 实例来源。代码里所有 ObjectMapper 是自动注入的,还是自己 new 的?自动注入的来自 Spring Boot 自动配置,通常没问题;自己 new 的很可能用的是旧构建方式,需要改造。
最后查序列化模块是否都来自 Jackson 3。有些自定义模块是从旧项目里复制的,import 改得不彻底,运行时会把 Jackson 2 的类注册到 Jackson 3 的 ObjectMapper 上,表现就是找不到类型或类型不匹配。
预防建议也很简单:升级前跑一遍依赖于 Jackson 2 的接口输出对比;升级后立刻检查依赖树中是否还有 com.fasterxml.jackson 开头的 artifact;在代码上线前保留一版升级前后的环境,方便做回归对照。
提示:不要把“编译通过”等同于“升级完成”。Jackson 3 的核心变化往往发生在行为层面,编译通过只是第一步,接口输出对比才是真正的验收标准。
最后聊一点我的个人选择。如果手里的项目是短期维护的老接口服务,我会让它留在 Spring Boot 3.x,不急着为升级而升级;如果是新项目或者有长期演进计划、准备上 GraalVM、准备切到 Spring Framework 7 这套新基线的,我会直接上 Spring Boot 4.0 加 Jackson 3。升级顺序上,我的建议是先改代码、后换依赖、再统一 classpath,每一步都跑一遍接口回归。重点永远是:不要在新旧 Jackson 混合的环境里做局部调试,先让 classpath 干净。Jackson 3 的未来主线很清晰,早一点摸清它的脾气,比晚一点在迁移窗口里赶工要舒服得多。