最近把一个跑了三年的 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 总结:我的迁移顺序
-
先升 Spring Boot 4.x,Jackson 版本交给 Boot 管理;
-
修编译错误:package 迁移(注意 annotations 是例外)+ 已删除 API;
-
删掉自定义 ObjectMapper Bean,回到 Boot 自动配置的 JsonMapper;
-
删掉 JavaTimeModule 相关依赖和注册代码;
-
老 SDK 用 spring-boot-jackson2 + use-jackson2-defaults 过渡,分两阶段关开关;
-
跑接口、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 十几年的包名,真的换了。
如果这篇帮你避开一次升级事故,帮忙点个"在看"和"星标",下次升级不踩坑~