SpringBoot4迁移:Jackson3包名大变,注解别乱改

最近把一个跑了三年的 Spring Boot 3.x 项目往 4.x 迁。出发前的预判是:最麻烦的应该是 Spring Security,或者一堆废弃的 Spring API。结果 pom 一改、依赖一拉,IDE 里最先红掉的,是全项目出现次数最多的那行 import------com.fasterxml.jackson.databind.ObjectMapper。

查了 Spring Boot 4 的迁移说明才确认:不是依赖没拉下来,是 Boot 4 默认已经从 Jackson 2 切到了 Jackson 3 。而且这不是一次普通的版本升级------Maven GroupId 和 Java Package 一起换了 ,com.fasterxml.jackson 大部分变成了 tools.jackson。

01 第一步:把 Codec 类改对,而不是只改编译通过

项目里 JSON 用得最重的是事件序列化,Kafka、Redis、第三方 HTTP API 都走它。老代码长这样:

JavaOrderEventCodec.java

import com.fasterxml.jackson.core.JsonProcessingException;

import com.fasterxml.jackson.databind.ObjectMapper;

@Component

public class OrderEventCodec {

private final ObjectMapper objectMapper;

public OrderEventCodec(ObjectMapper objectMapper) {

this.objectMapper = objectMapper;

}

public String encode(OrderCreatedEvent event) {

try {

return objectMapper.writeValueAsString(event);

} catch (JsonProcessingException e) {

throw new IllegalStateException("serialize failed", e);

}

}

public OrderCreatedEvent decode(String json) {

try {

return objectMapper.readValue(json, OrderCreatedEvent.class);

} catch (JsonProcessingException e) {

throw new IllegalStateException("deserialize failed", e);

}

}

}

Boot 3 时代这段代码遍地都是。到了 Boot 4,第一反应当然是改 import。但我最后没有继续注入通用 ObjectMapper,而是换成了更明确的 JsonMapper------这也是 Jackson 3 官方推荐的路线:JSON 用 JsonMapper,YAML 用 YAMLMapper,XML 用 XmlMapper,别再往一个通用 ObjectMapper 里塞各种 Factory。

JavaOrderEventCodec.java

import tools.jackson.databind.json.JsonMapper;

@Component

public class OrderEventCodec {

private final JsonMapper jsonMapper;

public OrderEventCodec(JsonMapper jsonMapper) {

this.jsonMapper = jsonMapper;

}

public String encode(OrderCreatedEvent event) {

return jsonMapper.writeValueAsString(event);

}

public OrderCreatedEvent decode(String json) {

return jsonMapper.readValue(json, OrderCreatedEvent.class);

}

}

这里有两个变化。一个显而易见:包名换了。另一个更值得注意------异常体系变了。

02 JsonProcessingException 没了:异常从 checked 变 unchecked

Jackson 2 时代,JsonProcessingException 几乎是每个序列化方法的固定搭配。因为它继承 IOException,是 checked 异常,编译器逼着你处理。于是几乎所有项目里都长着同一种没有业务意义的 catch------包一层 IllegalStateException 再抛出去。

Jackson 3 把它换成了 JacksonException,而且基础异常改成了 RuntimeException,不再继承 IOException。也就是说,升级之后,上面那种没有恢复能力的 try/catch 可以直接删掉:

JavaOrderEventCodec.java

// Jackson 3: 不再强制捕获, 没有恢复能力就让异常往外走

public String encode(OrderCreatedEvent event) {

return jsonMapper.writeValueAsString(event);

}

真正需要区分"JSON 解析失败"做降级的地方,再显式捕获:

JavaEventConsumer.java

import tools.jackson.core.JacksonException;

try {

return jsonMapper.readValue(json, OrderCreatedEvent.class);

} catch (JacksonException e) {

// 记录原始消息、进死信队列、或者走业务降级

throw e;

}

只是现在不再是编译器强迫你------异常处理回归了它本来的含义:只在有能力处理的地方处理。

03 最大的坑:注解没有全部换包名

看到 com.fasterxml.jackson → tools.jackson,很多人(包括第一次迁移的我)的第一反应是在 IDE 里全局 Replace。千万别。

Jackson 3 有一个刻意的例外:jackson-annotations 没有搬。

JavaUserResponse.java

// 这两个 import 在 Boot 4 里保持原样, 不要改!

import com.fasterxml.jackson.annotation.JsonIgnore;

import com.fasterxml.jackson.annotation.JsonProperty;

public record UserResponse(

@JsonProperty("user_id")

Long userId,

String nickname,

@JsonIgnore

String internalRemark) {

}

而且还有个更细的区分:普通的 @JsonProperty、@JsonIgnore、@JsonCreator、@JsonInclude 留在 com.fasterxml.jackson.annotation;但 databind 层的注解,比如 @JsonSerialize、@JsonDeserialize,跟着 databind 一起迁了:

Javaimport-compare.java

// 旧

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

// 新

import tools.jackson.databind.annotation.JsonSerialize;

我的建议:别做全局替换。先让 Maven 完成依赖升级,再让 IDE 按编译错误逐类修 import。 项目越大,这反而越安全------全局替换会把本来正确的注解改坏。

04 ObjectMapper 变 immutable:set/enable 那套写法过时了

老项目里几乎一定有这样一个配置类:

JavaJacksonConfig.java

@Configuration

public class JacksonConfig {

@Bean

ObjectMapper objectMapper() {

ObjectMapper mapper = new ObjectMapper();

mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

mapper.registerModule(new JavaTimeModule());

return mapper;

}

}

这种写法在 Jackson 3 里已经不推荐了------ObjectMapper 改成了 immutable + builder 设计。以前"创建实例后不断 set / enable / register"的路线,现在应该在 Builder 阶段把配置备齐,再一次性 build:

JavaJsonMapperFactory.java

JsonMapper jsonMapper = JsonMapper.builder()

.changeDefaultPropertyInclusion(inclusion ->

inclusion.withValueInclusion(JsonInclude.Include.NON_NULL))

.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)

.build();

原来的 mapper.copy() 也没了。想基于现有配置派生一个新 Mapper,用 rebuild():

JavaPrettyPrintTest.java

// 测试里需要 pretty 输出时, 从现有 Mapper 派生一个独立实例

JsonMapper prettyMapper = jsonMapper.rebuild()

.enable(SerializationFeature.INDENT_OUTPUT)

.build();

我后来把项目里的 Mapper 用法分成两类:业务代码统一注入 Boot 自动配置的 JsonMapper;只有测试、文件导出这类确实需要不同规则的地方,才用 rebuild() 派生独立实例。 比满项目 new ObjectMapper() 好控制得多。

05 自定义 ObjectMapper @Bean,建议直接删掉

说实话,大部分项目当年定义 ObjectMapper Bean,无非是为了配这几件事:忽略 null、日期格式、时区、枚举行为、未知字段、命名策略。这些 Spring Boot 的配置文件本身就能配:

YAMLapplication.yml

spring:

jackson:

default-property-inclusion: non_null

serialization:

write-dates-as-timestamps: false

确实需要代码级定制的,用 Boot 4 新的 JsonMapperBuilderCustomizer------它就是 Jackson2ObjectMapperBuilderCustomizer 的继任者:

JavaJacksonConfig.java

import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;

import tools.jackson.databind.SerializationFeature;

@Configuration

public class JacksonConfig {

@Bean

JsonMapperBuilderCustomizer jsonMapperCustomizer() {

return builder -> builder.disable(SerializationFeature.INDENT_OUTPUT);

}

}

Boot 4 文档专门提醒:自己提供一个 JsonMapper Bean,会整体替换掉自动配置的 Mapper,Boot 帮你注册的 Module、Converter 也会一起失效。所以我的选择是删掉自定义 Bean,能进配置文件的进配置文件。

06 JavaTimeModule 也可以顺手删了

还有一段祖传代码,估计很多项目里都有:

Javalegacy-snippet.java

ObjectMapper mapper = new ObjectMapper();

mapper.registerModule(new JavaTimeModule()); // 当年的固定搭配

当年不注册它,LocalDateTime 确实会出问题,于是这行成了 ObjectMapper 的固定搭配。Jackson 3 已经把 jackson-datatype-jsr310、jackson-datatype-jdk8、jackson-module-parameter-names整合进了 databind ,POM 里的依赖和这行 registerModule 可以一起删了。

07 老 SDK 还在 Jackson 2?可以共存,别硬迁

自己的代码能改,第三方 SDK 未必。升级途中就会出现这种局面:业务代码用的是 tools.jackson...JsonMapper,某个 SDK 却要求传入 com.fasterxml...ObjectMapper------名字一样,完全不是同一个类。

好在当初改 GroupId 和 package,本来就有让 2/3 共存的考虑。过渡期可以加上这个依赖:

XMLpom.xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-jackson2</artifactId>

</dependency>

再配合这个开关,让 Jackson 3 的 JsonMapper 尽量沿用 Boot 3 + Jackson 2 的默认行为:

YAMLapplication.yml

spring:

jackson:

use-jackson2-defaults: true

这个开关并不会把 Jackson 3 变回 Jackson 2,只是对齐旧行为。我的分阶段做法是:第一阶段开着它把代码迁完、接口测试全过;第二阶段单独关掉它,再跑一轮回归------这样能精确看出哪些 JSON 行为真的变了,而不是在一轮大升级里同时排查几十个问题。

08 升级回归,重点看的不是"能不能启动"

纯 REST 项目升级 Jackson,最多是前后端字段对不上。但如果 JSON 还参与了这些链路,影响就大得多:

  • Kafka 历史消息还能不能反序列化;

  • Redis 里以前缓存的 JSON 还能不能读;

  • 数据库 JSON 字段的读写行为;

  • 第三方接口签名用的 JSON 有没有变化;

  • 金额精度、日期格式、null 字段输出。

尤其是签名这种:

JavaPaymentClient.java

String body = jsonMapper.writeValueAsString(request);

String signature = sign(body, secret); // JSON 变了, 签名就变了

字段顺序、日期格式、null 输出任何一个变了,发出去的就是另一串数据,对方校验直接失败。这类代码我一定单独加一组升级回归测试,而不是只确认项目能编译。

09 总结:我的迁移顺序

  1. 先升 Spring Boot 4.x,Jackson 版本交给 Boot 管理;

  2. 修编译错误:package 迁移(注意 annotations 是例外)+ 已删除 API;

  3. 删掉自定义 ObjectMapper Bean,回到 Boot 自动配置的 JsonMapper;

  4. 删掉 JavaTimeModule 相关依赖和注册代码;

  5. 老 SDK 用 spring-boot-jackson2 + use-jackson2-defaults 过渡,分两阶段关开关;

  6. 跑接口、Kafka、Redis、第三方协议的 JSON 回归。

现在动手其实时机不错:Spring Boot 4.1.1 管理的是 Jackson 3.1.5,而 3.1 是 Jackson 3.x 的第一条 LTS 分支,比刚出 3.0 那会儿稳多了。如果项目还停在 Spring Boot 3.5,也该开始看了------3.5.16 已经是 3.5.x 最后一个 OSS 版本。

最后提醒一句:升级后 IDE 里那行红色的 com.fasterxml.jackson.databind.ObjectMapper,先别怀疑 Maven------它不是下载失败,是陪了 Java 十几年的包名,真的换了。

如果这篇帮你避开一次升级事故,帮忙点个"在看"和"星标",下次升级不踩坑~

相关推荐
SamChan901 小时前
PDF翻译时页眉页脚总在捣乱?跨页重复文本块的检测与过滤实测
人工智能·python·ai·pdf·wpf
徐健峰1 小时前
JEV 1.13 接入教程:Python 调用 Decisions API,实现分类、评分与 Agent 路由
人工智能
CubeSandbox1 小时前
沙箱是选项,不是标配:花椒 Agent 平台的架构思考与 Cube 实践
大数据·人工智能·架构
Rocky Ding*1 小时前
一文读懂Qwen-Audio-3.1核心基础知识:从语音识别到可控声景与实时Agent
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·qwen-audio
桃西西呀1 小时前
Laya 源码级原理拆解之四:路由检测与服务部署
人工智能·llm·ai编程
努力努力再努力wz1 小时前
【边缘计算入门系列】从“在哪里算”到“怎么算”:一文建立边缘计算、算子、计算图与 Tensor 的底层心智模型
人工智能·docker·边缘计算
黄啊码1 小时前
【黄啊码】一个陪伴类的 Agent 产品,凭什么我觉得它做得好?
人工智能
leoZ2311 小时前
第 34 篇 Copilot 与嵌入式 AI:把能力缝进工作流
人工智能·大模型·copilot·agent
桃西西呀1 小时前
Laya 源码级原理拆解之三:字段编译与业务胶水
人工智能·llm·ai编程