Spring Boot 4.0 正式把默认 JSON 处理库切换到 Jackson 3,这应该是最近 Java 后端圈子里讨论热度最高的一件事。Jackson 2 系列在这个生态里服役了十几年,猛然蹦出一个 3,还把包名从 com.fasterxml.jackson 换成了 tools.jackson,很多人的第一反应是:升级成本是不是要爆炸?我这两周把手头一个中等规模的模拟项目X完整迁了一遍,结论是:改动面确实比普通版本升级大,但对应关系十分清晰,并没有网上说的那么玄乎。这篇文章我就从 Spring Boot 4.0 为什么这样选、Jackson 3 到底改了什么地方、迁移时真正会踩到的坑三个角度,把这次切换完整拆一遍。适合正在做 Spring Boot 3.x 升级规划,或者被 Jackson 3 的包名变化吓住的同学参考。
1. 先搞清楚 Jackson 3 为什么“敢”动包名:这不是一次常规升级
1.1 坐标和包名全换了:受影响的不只是 import
很多朋友第一次接触 Jackson 3 时的反应是:版本号从 2 跳到 3,正常升级而已。结果一看 pOM 直接傻眼,原来的com.fasterxml.jackson.core:jackson-databind变成了tools.jackson.core:jackson-databind,源码里的 import 也从com.fasterxml.jackson.databind.ObjectMapper变成了tools.jackson.databind.ObjectMapper。
这不是改个版本号就完事的小升级,而是整个项目的坐标体系都换了。我不建议把它理解为“Jackson 项目改了个名”,更准确的说法是:项目换了新的门牌号,里面的户型结构虽然大体没变,但你按老地址去敲门,是敲不开了。下面是几个核心组件的坐标变化对比:
| 组件 | 2.x 坐标 | 3.x 坐标 |
|---|---|---|
| jackson-databind | com.fasterxml.jackson.core:jackson-databind | tools.jackson.core:jackson-databind |
| jackson-core | com.fasterxml.jackson.core:jackson-core | tools.jackson.core:jackson-core |
| jackson-datatype-jsr310 | com.fasterxml.jackson.datatype:jackson-datatype-jsr310 | tools.jackson.core:jackson-datatype-jsr310 |
| jackson-module-parameter-names | com.fasterxml.jackson.module:jackson-module-parameter-names | tools.jackson.core:jackson-module-parameter-names |
对应地,包名前缀也整体从com.fasterxml.jackson搬到了tools.jackson。这里我想强调的是:对 Spring Boot 4.0 来说,它依赖的已经不是“同一个库的新版本”,而是“换了一套坐标体系的同一个库”。所以spring-boot-starter-json内部引用的坐标跟着变,是必然的事。
1.2 API 层面清理了什么:废弃方法、Builder 化和默认行为
包名变化是用户感知最强的一层,但 Jackson 3 真正想做的事远不止“搬家”。在 API 设计层面,它把 2.x 里积累了多年的历史包袱做了一次大扫除。具体来说,几个比较明显的调整方向是这样的。
第一,大量已经被标记为废弃的方法和类被直接移除。2.x 时代为了兼容性总能留着的@Deprecated老接口,在 3.0 里没有继续留着的必要了。第二,Builder 模式成为创建对象的主流方式。2.x 中JsonMapper.builder()已经存在,但当时很多人还是习惯直接new ObjectMapper(),3.0 里官方更推荐通过 Builder 去配置特性、模块和过滤器。第三,部分特性(Feature)的默认值被重新调整。这一点非常容易踩坑,因为我实测下来,某些在 2.x 里默认开启或关闭的行为,在 3.x 里可能反过来了。官方文档写得很细,但没人会去背文档,最稳妥的方法就是保留一批测试用例来跑序列化结果对比。
还有一个容易被忽略的点:最低 Java 版本要求被拉高了。Jackson 3 把自己的兼容基线提得比 2.x 更激进,等于变相淘汰了一些非常老旧的 JDK 环境。而 Spring Boot 4.0 本来就把基线定在 Java 17 以上,两者在这个层面的步调是一致的。
1.3 为什么 Spring Boot 4.0 要在这个时间点接住 Jackson 3
很多人会问:Jackson 3 的包名和坐标变化这么大,Spring Boot 官方团队为什么愿意在这个版本里全面拥抱它?我的理解是:大版本窗口是用来消化破坏性变更的。
Spring Boot 4.0 本身对应着 Spring Framework 7,已经是一次足够大的版本跃迁。既然用户迟早要面对 Spring Boot 的破坏性变更,那不如把依赖层的重大变更也放在同一时间点一起解决。如果 Spring Boot 4.0 仍然沿用 Jackson 2,而 Jackson 3 又要在这个生态里长期存在,那未来三五年内就会出现“Boot 新版本 + 老 Jackson”的奇怪组合,维护成本更高。
把两件破坏性的事合并到一次升级里,短期看用户要承受的痛感是叠加的,但长期看是划算的。因为你不必经历两次大迁徙:先升 Boot 4,再升 Jackson 3。一步到位反而是更理性的选择。
2. Spring Boot 4.0 集成 Jackson 3 的底层逻辑与自动配置变化
2.1 starter-json 不再默默带旧坐标:依赖树先看清楚
Spring Boot 项目里大多数 JSON 序列化能力都不是直接依赖 Jackson,而是通过spring-boot-starter-web或spring-boot-starter-json传递引入的。Spring Boot 4.0 的spring-boot-starter-json已经把内部坐标切到了 Jackson 3。这意味着,如果你的项目用了 Starter Web,那依赖树里会自动出现tools.jackson.core:jackson-databind,不再需要手动声明。
但有个隐患我要提醒:如果你的项目里之前为了图方便,直接在 pom.xml 里显式声明了com.fasterxml.jackson.core:jackson-databind,升级 Boot 4.0 后这个旧坐标仍然会被 Maven 解析进去。结果就是项目里同时存在新老两套 Jackson,运行时报错会让你一头雾水。所以升级时先跑一遍依赖树看清楚,比什么都重要。
mvn dependency:tree -Dincludes=tools.jackson.* mvn dependency:tree -Dincludes=com.fasterxml.jackson.*两条命令可以分别看到新老坐标的存在情况。理想状态是:新坐标出现,旧坐标只剩那些无法避免的传递依赖。
2.2 自动配置从 2.x 切到 3.x,ObjectMapper Bean 的生成路径变了
Spring Boot 的自动配置核心类是JacksonAutoConfiguration,它在 Spring Boot 3.x 里注册的是基于com.fasterxml.jackson的ObjectMapperBean。到了 Spring Boot 4.0,这个自动配置类整体对齐到了tools.jackson下的对象模型和模块体系。
具体的变化体现在几个方面:第一,自动配置会注册一个ObjectMapper类型的 Bean,但这个 ObjectMapper 的实际运行类来自 Jackson 3,底层包名已经完全不同。第二,Boot 默认注册的几个常用模块,比如处理 Java 8 日期时间类型的JavaTimeModule、处理 JDK8 新类型的Jdk8Module、处理参数名保留的ParameterNamesModule,模块类本身都从新包名下重新引入了。第三,spring.jackson.*系列配置项(比如日期格式、时区、序列化包含规则)在新版本里依然可用,但底层映射到的 Feature 枚举类型已经换了包。
如果你的项目里没有自定义 ObjectMapper,这些变化对业务代码完全透明。你会发现自己根本没有直接接触过那些类。但如果你像很多人一样,自己写了一个配置类去自定义 ObjectMapper,那这就是本次升级里工作量最集中的地方。
2.3 自定义 ObjectMapper 的迁移:一个典型配置类的改动前后
我见到的项目里,最常见的自定义配置是这样的:自己 new 一个 ObjectMapper,注册 JavaTimeModule,然后把日期序列化格式改成 ISO。迁移前后代码对比如下。
迁移前:
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } }迁移后:
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.SerializationFeature; import tools.jackson.datatype.jsr310.JavaTimeModule; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } }只改 import 就能跑通的情况占大多数,因为 ObjectMapper 的核心 API 在 3.0 里保留了 2.x 的绝大部分用法。不过我更推荐顺手改成 Jackson 3 主推的 Builder 方式:
import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.SerializationFeature; import tools.jackson.databind.json.JsonMapper; import tools.jackson.datatype.jsr310.JavaTimeModule; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { return JsonMapper.builder() .addModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build(); } }这里有一个特别容易出问题的点:如果项目里已经存在自定义 ObjectMapper Bean,Spring Boot 的自动配置会因为这个 Bean 的存在而整体“退位”,默认模块一个都不会帮你注册。很多人升级完以后发现 LocalDateTime 序列化结果变成了一串数组数字,就是因为自定义 Bean 里只注册了 JavaTimeModule,漏掉了其他模块,或者连 JavaTimeModule 都忘了注册。这一点在 Spring Boot 3.x 时代就存在,4.0 依然如此,迁移时务必仔细核对。
3. 迁移实战:把现有项目从 Jackson 2 切换到 Jackson 3 的完整步骤
3.1 迁移前盘点:三个动作摸清影响面
升级前别急着动手改代码,先花半小时把影响面摸清楚。我的做法是三个动作并行。
第一个动作是全量搜索源码里的旧包名。直接搜com.fasterxml.jackson这个字符串,统计它出现在哪些文件里、哪些是业务代码、哪些是第三方依赖里的代码(第三方代码搜不到,但可以通过反编译或依赖源码确认)。大多数中型项目里,这个数字大概在几十到上百之间,不用被吓到,大部分只是 import 行。
grep -rn "com.fasterxml.jackson" --include="*.java" .第二个动作是全量检查 pom.xml 或 build.gradle 中的依赖声明。重点看有没有显式声明 Jackson 2 系列的坐标,以及有没有在某个模块里写了<exclusions>排除 Jackson 相关的传递依赖。这些地方都是升级时的隐藏地雷。
第三个动作是确认自定义配置的数量。搜索ObjectMapper、ObjectMapperCustomizer、MappingJackson2HttpMessageConverter这些关键词出现的类,逐一评估它们是否依赖了旧包名。特别要注意的是,如果你在代码里直接 new 过MappingJackson2HttpMessageConverter或者自定义过消息转换器,升级到 4.0 后的类名或构造方法参数可能都要重新核对。
3.2 依赖替换和 import 替换:能全局处理就别手写
盘点完以后就可以开始动手了。我的顺序是先改构建配置,再改代码,最后跑测试。改构建配置时,我会做一个不太激进的操作:不直接删除所有旧坐标,而是先把新的坐标引入,让依赖树同时存在新老两套,等所有代码编译通过后再回头清理旧坐标。这样做的目的是减少一次升级中同时引入的变量数量,排查起来更容易。当然,如果你对项目很有把握,也可以一步到位直接替换。
pom.xml 里关键的替换是这样:
<dependency> <groupId>tools.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>如果之前显式声明过旧坐标:
<!-- 这是要删掉的 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>删掉旧坐标时记得检查它被用在哪些地方,如果只是传递依赖,那直接删除声明即可。
代码替换方面,我强烈建议使用 IDE 的全局替换功能,而不是手写一个个改。把com.fasterxml.jackson全局替换成tools.jackson,替换范围限定在*.java文件。大部分情况下这是一次安全的全局操作。但也有例外:如果你在字符串字面量、配置文件(比如 application.yml 里的jackson配置项,注意是配置项不是包名)或者注解参数里写死了旧包名,全局替换可能会误伤。所以替换完以后不要直接编译,先看一眼 diff 再继续。
3.3 序列化行为一致性验证:重点盯日期、空值、多态
编译通过只是一个开始,真正的工作量在行为验证上。我自己项目里当时遇到的第一处差异就是日期时间序列化。换到 Jackson 3 后,如果某些特性默认值发生了变化,接口返回的 JSON 可能从 ISO 字符串变成了时间戳,或者反过来。这个差异在联调阶段很难发现,因为前后端约定的接口文档上写的还是旧格式。
我建议升级后专门做一轮接口快照对比。做法不复杂:在升级前,用旧版本启动项目,对核心接口调用一遍,把返回的 JSON 原样保存到一批“黄金文件”里。升级后再次调用同样的接口,生成新的 JSON,然后用 diff 工具对比新旧输出。这个动作能帮你把绝大多数行为差异暴露在测试阶段,而不是线上告警里。
对比时需要重点关注三类字段:第一类是日期和时间,看格式是否一致;第二类是空值和空集合,看是否被NON_NULL、NON_EMPTY之类的配置影响,多出或减少了字段;第三类是多态类型的序列化,也就是带有@JsonTypeInfo的抽象类或接口字段,看类型标记字段是否仍然保留、是否变更了默认值。
diff -r old-snapshots/ new-snapshots/如果 diff 结果里有差异,不要慌,先看差异是否来自我们主动调整的配置,还是来自 Jackson 3 的默认行为。如果是后者,就需要在自定义配置里显式声明期望的特性设置。
3.4 第三方依赖冲突处理:别盲目排除老版本
整个迁移过程中最让我头疼的不是自己的代码,而是第三方依赖。项目里有一个内部 SDK,编译期的 jar 里直接依赖了 Jackson 2 的类。当我全局排除掉com.fasterxml.jackson相关依赖以后,这个 SDK 在运行时直接抛NoClassDefFoundError,排查了很久才发现是它的内部实现需要调用旧包名下的类。
这类问题的处理思路不能是“无脑排除老版本”。因为排除依赖只是把传递依赖挡在 classpath 之外,如果第三方库在运行时真的需要这些类,反而会让它在启动时报错。正确的做法是分情况处理:如果第三方库可以升级到支持 Jackson 3 的版本,优先升级;如果暂时升不了,就用插件把依赖里的 Jackson 2 重新定位到隔离的包路径里,让项目里同时存在新老两套 Jackson,互不干扰。另一种更省事的方案是等第三方库的维护者跟上节奏,但这取决于你所处的业务环境。
这里我有一个实际的建议:升级前先把项目里所有直接和间接依赖 Jackson 的组件列成一张表,标注清楚每个组件的升级路径。这张表能帮你在排查 classpath 冲突时节省大量时间。
4. 常见问题与排查技巧实录
4.1 我踩过的版本冲突坑:一条 NoClassDefFoundError 的排查记录
我还是具体讲讲那次踩坑吧。升级完 Spring Boot 4.0 以后,项目编译是过了,但有一个接口在调用时直接 500,日志里只有一行NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper。
我当时第一反应是代码里还有漏网的旧包名没替换完。但搜索了一圈,业务代码里已经没有com.fasterxml开头的 import 了。后来用mvn dependency:tree一看才发现,出问题的类来自一个内部 SDK,SDK 打包时把 Jackson 2 的 ObjectMapper 直接编译进了它的字节码里。由于我在这之前把系统中所有 Jackson 2 的坐标都去掉了,SDK 运行时就找不到类了。
这个问题的根因不是“Jackson 3 兼容性差”,而是版本冲突治理没做到位。正确做法我在前面已经提过:要么推动这个 SDK 升级,要么在构建配置里单独给它保留旧版 Jackson。我也在项目里见过更粗暴的解决方案——直接把 SDK 源码里用到 Jackson 2 的类复制出来改成本地类,临时过渡。这种做法虽然脏,但在升级窗口非常紧的时候,确实能救命。
4.2 经典报错与处理方式速查
我把这次迁移过程中遇到过的典型问题整理成了一张速查表,方便升级过程中对照排查。
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 编译报错“程序包com.fasterxml.jackson.databind不存在” | import 没替换干净或旧坐标未完全清理 | 全局搜索旧包名,替换为 tools.jackson;检查 pom |
| 运行时 NoClassDefFoundError 指向 com/fasterxml/jackson/... | 第三方依赖或本地代码仍引用旧包名 | 排查依赖树,为对应依赖保留旧版 Jackson,或推动升级 |
| LocalDateTime 序列化变为数字数组 | 自定义 ObjectMapper 未注册 JavaTimeModule | 补齐模块注册,或删除自定义 Bean 让 Boot 自动配置接管 |
| 序列化输出多了或少了字段 | 空值处理默认行为差异 | 显式配置 spring.jackson.default-property-inclusion |
| 多态类型字段序列化结果变化 | @JsonTypeInfo 相关默认行为调整 | 在注册模块或注解上显式指定 include/use 属性 |
| JacksonAutoConfiguration 不生效 | 项目里有自定义 ObjectMapper Bean | 删除自定义 Bean,改用 ObjectMapperCustomizer 方式定制 |
| IDEA 一直提示 tools.jackson 不存在 | 构建缓存未刷新 | Maven/Gradle reimport,并检查仓库镜像是否正确 |
4.3 升级检查清单:可以照抄的那一种
最后给一份可以直接抄走的检查清单,我每次做这类升级都会按它过一遍,减少漏项。
- 构建文件里确认 Starter JSON 的传递坐标已经来自 tools.jackson 体系
- 所有业务源码中
com.fasterxml.jacksonimport 的数量为 0 - pom.xml / build.gradle 中旧的 Jackson 2 显式坐标已删除或明确保留原因
- 依赖树检查结果中旧坐标只来自被隔离的第三方组件
- 自定义 ObjectMapper 配置已确认模块注册完整,且日期、空值行为符合预期
- 核心接口的快照对比测试已执行,新旧 JSON diff 结果已人工确认
- 多态、日期、泛型这些容易出问题的边界情况已有针对性测试覆盖
- 灰度环境已部署,监控了序列化相关的错误日志和接口耗时
这份清单适用于绝大多数 Spring Boot 项目。你要是项目比较特殊,比如重度使用 Kotlin 序列化或者有大量自定义 Jackson 模块,那还要额外关注对应模块在 3.x 下的兼容情况。
最后再分享一个我个人的体会。整个迁移过程做完以后,回头看我花时间最多的地方不是改 import,也不是调配置,而是花在“信任验证”上。代码层面的改动是可以穷尽的,但序列化行为这样的隐性问题,只有靠真实数据的对比测试才敢放心。所以如果你也要做这次升级,别心疼那点写快照测试的时间,这笔投入一定是值得的。