Jackson 3 换的是包名:共存期怎么算
判断先给:评估要不要上 Jackson 3,工作量别押在「换依赖、改 import」上——那部分是机械活,全局检索加批量替换,一两天做得完。共存期真正吃人力的是另外两件事:2 与 3 序列化行为对不上、但构建和启动都不拦你;依赖树里有库还钉在 2 上,收敛节奏不归你定。
换包名是共存的前提,成本因此挪到了行为差异
按多个公开来源互相印证的信息:Jackson 3 把 Java 包名从 com.fasterxml.jackson 换成了 tools.jackson,主要入口从 new ObjectMapper 换成 builder 风格的 JsonMapper,日期时间等默认行为有调整;Spring Boot 4 已默认切到 Jackson 3,伴随配置项改名与自动配置调整。Boot 4 怎么迁、逐项清单,我写在另一篇里(Spring Boot 4 迁移),这篇不重复,只处理其中最容易低估的一层:共存。
包名换掉带来的架构后果是别的升级都没有的:全限定类名不同,2 和 3 在同一条 classpath 上并存不冲突。所以 Boot 4 能给出 jackson2 兼容模块当正式退路,而不是过渡性的权宜。我手上这个仓库(Boot 4.0.7)能看到切换后的真实形态:注解类仍按 com.fasterxml.jackson.annotation 导入——这一组 artifact 的包名没动——mapper 类已经在 tools.jackson.databind 下,两种包名的 import 并排出现在同一个源文件里,跑得正常。
这个组合决定了共存期的工作内容:不是「哪一天拨开关」,而是「两套序列化引擎同时在跑,各跑在哪里、差异在哪」。
成本从换依赖那一刀,挪到了行为差异
调用点长在哪几层,拿本仓库当样本:主干代码直接碰到 mapper 的只有四处。三处走统一入口 JsonUtil.getObjectMapper()——web 层错误响应的写出、AI 响应的解析、路由表配置的读取;一处是给备份归档格式专用、用 JsonMapper.builder() 现造的 mapper,显式配了 snake_case 命名、字段可见性收窄、读侧未知字段严格失败、混入剔除瞬态字段。测试里另有六十三处断言,全部走那个统一入口。
一个中等体量的 Spring 项目,这类调用点通常就长在三层:JSON 工具类的统一入口、文件/归档/对外协议专用的定制序列化、测试断言。第一层收得好就一处翻两面;第三层不吃亏,行为差异会被测试先撞出来;危险的是第二层——每一处显式配置都是一个 2 和 3 可能理解不同的点,而它错了不报错,只是输出不一样。
「报错」和「不报错」的分界,值得多说一句。编译断的东西反而好办:清单是封闭的,改一条验一条;静默漂移没有清单,你只能靠基线把它逼出来。所以共存期最坏的画面不是构建红着,而是应用照常启动、接口照常返回 200,只是某类字段——时间的写法、空字段出不出现、多传的键收不收——在两套引擎下各说各话,而下游拿到的报文没有版本号可看。落库存成 JSON 的字段更进一层:写它和读它的引擎一旦分家,中间隔着一次发布,旧数据在新栈上读不出来时,报错也来得又晚又间接。
一张对照表:哪些当场报错,哪些跑起来才发现
| 2 与 3 的差异点 | 会不会当场报错 | 怎么先探一遍 |
|---|---|---|
core / databind 包名换成 tools.jackson |
会,编译直接断 | 全局搜 import;mvn dependency:tree 看谁还带进旧坐标 |
| 注解 artifact 的包名没变 | 不用报,注解两边共用 | 注解代码不动,但要确认没有库只认旧 databind 的注解处理器 |
ObjectMapper 换成 builder 风格的 JsonMapper |
直接 new 或引类型的会编译断;藏在工具类里的只需改一处 | 搜 new ObjectMapper 和注入点,数一下真正散落的有几处 |
| 日期时间等默认行为调整 | 不会,照样编译启动,输出不一样 | 下面那三类断言,在旧栈上先跑一遍留底 |
| 三方库还留在 2 上 | 不会,两套并行各自序列化 | 列依赖树,再逐个看它的 API 边界暴不暴露 2 的类型 |
| Boot 侧配置项改名、自动配置调整 | 多数是键写错静默忽略 | 在 application.yml 和配置中心全量搜 jackson 相关键,按迁移文档逐键核对 |
顺带一提,代码里自己写的定制组件也在这张表的「不报错」列里——注解改名那类,源文件不在依赖树里,只能靠全局检索兜。本仓库的配置面很窄,application.yml 的 jackson 段只有一行(响应信封不输出空字段),加上归档那一处代码内配置,核对清单就是这两条——配置面窄的项目探起来快,这本身就是「要不要现在动」的一个判据。
先探一遍:两份清单加三类断言
要动手之前,下面这几步全部在现有栈上做完,产出的都是事实不是猜测:
- 把还留在 2 上的库列出来。 依赖树里过滤
com.fasterxml.jackson开头的坐标,注解那一组除外(它本来就不搬家)。数量看完看性质:一个库如果公开方法签名直接收或返 2 系的ObjectMapper、JsonNode,它会把调用方一起拖到 2 那边,这类才决定共存的终点在哪天;只在自己内部序列化、类型不出现在 API 边界的,多留一阵无害。 - 跨界对象盘点。 共存期同一个 DTO 会被两套引擎分别读写——对外接口报文、消息体、落库的 JSON 字段。把这份对象清单拉出来,下面三类断言对每个对象在 2 和 3 上各跑一遍,逐字段 diff,差异要么归零要么留档成显式决定。
- 三类序列化断言,在测试里先跑:
- 日期时间:固定时刻的
Instant、LocalDateTime往返序列化,记下输出形态(数字时间戳还是 ISO 字符串)、时区、序列化走的模块。3 在这块默认行为有调整,这是最可能的漂移点。 - 空值与字段可见性:缺省包含策略、只有 getter 的对象和只有字段的对象各输出什么。命名策略与可见性在 builder 风格下改成了显式配置,理解不一致就从这里出。
- 严格与宽松:多一个未知键的读入行为、
BigDecimal的尾数处理、未知枚举值。这几项不保证一定不同,但都要留底,因为「静默成功」正是这类差异的签名。 留底的做法:三类断言先在现有栈上跑一遍,把输出固化成基准文件,换栈后跑同一组对 diff。这样「差异」是个文件级的事实,不是某次联调时的印象。
- 日期时间:固定时刻的
- 给共存定一个收敛信号。 依赖树里注解之外、仍是旧坐标的 artifact 数量趋零,共存期结束;在此之前,别信「编译过了就等于迁移完了」。发现差异之后的处置也只有两条路:能配置拉平的,拉平到旧行为、保住线上报文不变;拉不平的,把它当一次显式的格式变更走数据兼容,而不是顺手改掉。
跟与不跟,各自什么条件成立
跟,需要三条同时成立:本来就要迁 Boot 4(Jackson 3 是随行的,不是一个独立决策,要回答的只是共存边界画在哪);依赖树里没有库在 API 边界上暴露 2 的类型,或升级版本已在路上;上面那三类断言有能跑的基线,CI 能出 diff。
不跟,也是三条:测试稀到抓不住行为差异——那「能共存」这个前提反而害你,两套都跑着、漂移没人看见;关键库的新版没影——你先动,等于替上游维护两套边界的对接;以及最该说清的一条,假如上面都不成立,包名共存这个特性本身就支持你不动:它给的是「可以按自己节奏推进、不必一次全换」,2 和 3 在 classpath 上从来不互相占位,等,是不花钱的。
还有一条送给决定不跟的团队:上面那三类断言本身值得跑。它等于给现有序列化行为写了一份特征测试,跟不跟 3 都该有;有了它,下次任何人——包括升级 Boot 的你自己——动了序列化,你都看得见。
一句收尾
Jackson 3 换的是包名,不是你的判断力。共存意味着不再有「切换那一天」,只剩收敛节奏:盘完依赖树里谁还暴露着旧类型,跑完那三类断言拿到 diff,条件齐了就随 Boot 4 一起走,不齐就两套并行着等——要盯的从来不是包名,是行为差异有没有人在看。
本文作者:Aryee 发布时间:2026-09-27 13:26
本文出处:ARYEE.cn 固定链接:https://www.aryee.cn/archives/jackson-3-coexistence.html
转载或引用请连这一行一起带走;文中的代码与结论按当时环境成立,组件升级后请以官方文档为准。