下班之后没人亲亲,敲段代码哄自己开心。写个demo修完bug,誓让女神喊你哥哥。
Spring Boot 4 默认带的 Jackson 升到了 3.0。还没有升级的小伙伴们可以提前了解下变化,这次升级不是加几个方法的小迭代:包名从 com.fasterxml.jackson 换到了 tools.jackson,ObjectMapper 的用法变了,十来个默认值变了,如果不知道如何配置,连序列化性能都跟着掉了一截。
如果你还在用 Spring Boot 2,大概率还停留在 new ObjectMapper() + 手动注册 JavaTimeModule 的写法。这篇文章把 Jackson 3 相对 Jackson 2 的变化总结一下,看完能知道改了什么、怎么改、哪些坑别踩。
背景很简单:Jackson 2.x 从 2012 年用到现在,攒了十几年技术债,对 Java 17+ 的 Record、sealed class 这些新特性支持一直别扭。Jackson 3 是一次从底层重构。Spring Boot 3 那次升级主要搞了 javax.* 到 jakarta.* 的 Jakarta 迁移,Spring Boot 4 这次搞的就是 Jackson 迁移。从 Boot 2 到 Boot 4 你跨了两个大版本,Jackson 是和你日常代码贴得最近的一环。
一、包名和依赖全换了
升级 Jackson 3,第一件事就是 import 全报红。
Maven 坐标
| 项目 | Jackson 2.x | Jackson 3.x |
|---|---|---|
| GroupId | com.fasterxml.jackson | tools.jackson |
| core | com.fasterxml.jackson.core:jackson-core | tools.jackson.core:jackson-core |
| databind | com.fasterxml.jackson.core:jackson-databind | tools.jackson.core:jackson-databind |
| annotations | com.fasterxml.jackson.core:jackson-annotations | 不变,仍用 2.x 版本线 |
annotations 没换包名,是个例外。Jackson 团队评估后觉得 annotations 模块的 API 够稳定,没必要硬迁,所以 jackson-annotations 还留在 2.x 的包名和版本线上。在项目里继续用 com.fasterxml.jackson.annotation.JsonProperty,不用改。
Java 包名
// Jackson 2.x(Spring Boot 2/3)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.core.JsonProcessingException;
// Jackson 3.x(Spring Boot 4)
import tools.jackson.databind.ObjectMapper; // 包名变了,类还在
import tools.jackson.databind.JsonNode;
import tools.jackson.core.JacksonException; // 异常改名了
迁移第一步就是全局替换包名,IDE 的全局替换能搞定大部分。注意别把 annotations 包也替换了,它还是 com.fasterxml.jackson.annotation。
spring-boot-starter-json 在 Spring Boot 4 里底层换成了 Jackson 3,依赖配置不用改。项目使用了 Spring Boot 4 直接会使用 Jackson 3 。
二、ObjectMapper 分家,JsonMapper 成了 JSON 的主角
ObjectMapper 没被删,包名变成了 tools.jackson.databind.ObjectMapper。
变化在于设计思路——以前一个 ObjectMapper 干所有格式,现在按格式分家:
| 格式 | Jackson 2.x | Jackson 3.x |
|---|---|---|
| JSON | ObjectMapper | JsonMapper(推荐) |
| XML | XmlMapper | XmlMapper(独立模块) |
| YAML | YAMLMapper | YAMLMapper(独立模块) |
JsonMapper 继承自 ObjectMapper,专精 JSON,是推荐入口。
不可变 + Builder
// Jackson 2.x:ObjectMapper 可变对象,new 完随便改
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
String json = mapper.writeValueAsString(user);
// Jackson 3.x:JsonMapper 不可变,Builder 一次性构建
JsonMapper mapper = JsonMapper.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // disable 而非 configure
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.build(); // 必须显式 build
String json = mapper.writeValueAsString(user);
JsonMapper 是不可变的,不能 new 出来再改配置,得通过 Builder 一次性构建。想改配置用 rebuild() 基于现有实例复制一个新的:
JsonMapper newMapper = mapper.rebuild()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
这跟 Java 11 里 HttpClient 的设计思路一样,不可变对象天然线程安全,不用再纠结”ObjectMapper 能不能单例共享”这种老问题。现在它就是必须当单例用,因为你也改不了它。
简单场景下可以直接用 JsonMapper.shared(),返回一个默认配置的共享实例,省得自己 Builder。
其他核心类重命名
| Jackson 2.x | Jackson 3.x | 说明 |
|---|---|---|
JsonFactory | TokenStreamFactory | 流式 API 的工厂 |
JsonSerializer / JsonDeserializer | ValueSerializer / ValueDeserializer | 命名更直观 |
SerializerProvider | SerializationContext | 序列化上下文 |
Module | JacksonModule | 模块接口 |
TextNode | StringNode | JSON 字符串节点 |
MappingJacksonValue | 移除 | 用 .hint() 替代 |
JsonSerializer 改名 ValueSerializer 挺合理,它序列化的确实是”值”而非”JSON 结构”。代价是所有自定义序列化器的 import 和类声明都得动。
三、十来个默认值变了
这是最容易出 Bug 的地方。Jackson 3 把一批默认值反过来了。
默认值变更对照
| 特性 | Jackson 2.x 默认 | Jackson 3.0 默认 | 影响 |
|---|---|---|---|
FAIL_ON_UNKNOWN_PROPERTIES | true | false | JSON 多字段不再报错,可能吞掉数据问题 |
DEFAULT_VIEW_INCLUSION | true | false | 没有 @JsonView 的字段默认不序列化 |
FAIL_ON_EMPTY_BEANS | true | false | 空对象不再报错,直接序列化成 {} |
WRITE_DATES_AS_TIMESTAMPS | true | false | 日期输出 ISO-8601 字符串而非时间戳 |
SORT_PROPERTIES_ALPHABETICALLY | false | true | JSON 字段按字母排序,可能影响接口契约 |
FAIL_ON_TRAILING_TOKENS | false | true | JSON 末尾多余内容会报错 |
ALLOW_FINAL_FIELDS_AS_MUTATORS | true | false | final 字段不再被当作 setter 反序列化 |
READ_ENUMS_USING_TO_STRING | false | true | 枚举用 toString() 而非 name() 反序列化 |
WRITE_ENUMS_USING_TO_STRING | false | true | 枚举输出用 toString() |
FAIL_ON_NULL_FOR_PRIMITIVES | false | true | null 赋给基本类型会报错 |
几个常用的重点:
FAIL_ON_UNKNOWN_PROPERTIES 变成 false:以前 JSON 多字段会报错,现在默默忽略。好处是不用再写 @JsonIgnoreProperties(ignoreUnknown = true);如果你靠这个报错来发现前后端字段不一致,现在它不报了。
WRITE_DATES_AS_TIMESTAMPS 变成 false:以前日期输出 1719504000000,现在输出 "2024-06-28T00:00:00"。前端如果在解析时间戳,升完级直接炸。这是最常见的线上事故来源,升级后第一件事就是检查日期输出格式。
FAIL_ON_NULL_FOR_PRIMITIVES 变成 true:以前 null 反序列化到 int 字段会静默赋 0,现在直接抛异常。改动本身合理,null 和 0 是两码事,但老代码可能正靠这个”特性”苟着。
// Jackson 2.x:没问题
// Jackson 3.x:如果 JSON 中 age 是 null,直接抛异常
public class User {
public int age; // 基本类型 + null = 爆
}
改成 Integer,或者显式 disable 这个特性。
四、异常变化,catch IOException 的代码全得改
// Jackson 2.x:JsonProcessingException 继承 IOException
try {
User user = mapper.readValue(json, User.class);
} catch (IOException e) { // 能捕获 Jackson 异常
log.error("反序列化失败", e);
}
// Jackson 3.x:JacksonException 继承 RuntimeException
try {
User user = mapper.readValue(json, User.class);
} catch (IOException e) { // 捕获不到了
log.error("这行永远不会执行", e);
}
// 得显式捕获
try {
User user = mapper.readValue(json, User.class);
} catch (JacksonException e) {
log.error("反序列化失败", e);
}
异常类也重命名了:
| Jackson 2.x | Jackson 3.x |
|---|---|
JsonProcessingException | JacksonException(继承 RuntimeException) |
JsonMappingException | DatabindException |
JsonParseException | StreamReadException |
JsonGenerationException | StreamWriteException |
官方的解释是:JSON 序列化反序列化不是 I/O 操作,在内存里转个对象不该抛 IOException。逻辑上说得通, catch (IOException e) 全得改。
方法签名上的 throws JsonProcessingException 可以直接删掉。
五、被删的 API
Jackson 3 正式删掉了一批早就该退休的 API,还在用的话编译直接报错。
| 被移除的 API | 替代方案 |
|---|---|
ObjectMapper.canSerialize() / canDeserialize() | 无直接替代,按需处理 |
JsonNode.fields() | JsonNode.properties() |
ObjectMapper.setSerializationInclusion() | Builder 中 .serializationInclusion() |
MappingJsonFactory | 用 TokenStreamFactory |
DefaultTyping.EVERYTHING | 改用 DefaultTyping.NON_FINAL 或自定义 |
MapperFeature.USE_STD_BEAN_NAMING | 已成默认行为,无需配置 |
MapperFeature.AUTO_DETECT_xxx 系列 | 用 @JsonAutoDetect 显式控制 |
MapperFeature.USE_STD_BEAN_NAMING 被移除是因为它现在就是默认行为,Jackson 3 默认用标准 Java Bean 命名,不再有”MVC 风格”和”标准风格”的歧义。
六、Spring Boot 4 里的 Jackson 3 自动配置
注解和类重命名
| Spring Boot 2/3 | Spring Boot 4 | 说明 |
|---|---|---|
@JsonComponent | @JacksonComponent | 组件注解 |
@JsonMixin | @JacksonMixin | Mixin 注解 |
JsonObjectSerializer | ObjectValueSerializer | 序列化器 |
Jackson2ObjectMapperBuilderCustomizer | JsonMapperBuilderCustomizer | Builder 定制器 |
配置前缀多了一层
# Spring Boot 2/3(Jackson 2)
spring:
jackson:
read:
fail-on-unknown-properties: true
write:
dates-as-timestamps: false
# Spring Boot 4(Jackson 3)
spring:
jackson:
json:
read:
fail-on-unknown-properties: true # 多了 json 层级
write:
dates-as-timestamps: false
Java 8 模块终于不用手动注册了
Jackson 2 时代用 Java 8 日期时间 API,得手动注册三个模块:
// Jackson 2.x
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule()); // JSR-310 日期时间
mapper.registerModule(new Jdk8Module()); // Optional 等
mapper.registerModule(new ParameterNamesModule()); // 构造器参数名
Jackson 3 里这三个模块全部内置,通过 JDK Service Loader 自动发现,classpath 里有依赖 JsonMapper 就自动加载。
// Jackson 3.x
JsonMapper mapper = JsonMapper.shared();
// JavaTimeModule、Optional 支持、构造器参数名 都有了,不用注册
两种兼容方案
方案一:恢复 Jackson 2 默认行为
spring:
jackson:
use-jackson2-defaults: true # 回退到 Jackson 2.x 的默认值
加了这行,Jackson 3 的默认值会回退到 2.x 行为。过渡方案,别长期依赖。
方案二:临时用回 Jackson 2
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson2</artifactId>
</dependency>
引入后 Spring Boot 回退到 Jackson 2,配置前缀变成 spring.jackson2.*。最保守的路线,先升 Spring Boot 4,Jackson 继续用 2.x,准备好了再迁。
Jackson 2(com.fasterxml.jackson.*)和 Jackson 3(tools.jackson.*)包名不同,两个版本能共存。代价是包体积多 3-4MB,混用容易出 bug。
七、默认配置有性能坑,从别人那看到的坑
Jackson 3 换了内部的 RecyclerPool 实现,从 Jackson 2 的 ThreadLocalPool 换成了基于 deque 的 RecyclerPool。
JMH 基准测试
| 场景 | Jackson 2 | Jackson 3(默认) | Jackson 3(优化) |
|---|---|---|---|
| 序列化 | 7297 ops/ms | 4631 ops/ms(↓36.5%) | 6614 ops/ms(恢复至 90.6%) |
| 反序列化 | 2431 ops/ms | 2280 ops/ms(↓6.2%) | 2350 ops/ms(恢复至 96.7%) |
默认配置下序列化吞吐量下降约 36.5%,反序列化影响小一些(约 6%)。
优化:换回 ThreadLocalPool
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.util.JsonRecyclerPools;
import tools.jackson.databind.json.JsonMapper;
@Configuration
public class JacksonConfig {
@Bean
public JsonMapper jsonMapper() {
JsonFactory factory = JsonFactory.builder()
.recyclerPool(JsonRecyclerPools.threadLocalPool())
.build();
return JsonMapper.builder(factory).build();
}
}
换回 threadLocalPool 后,序列化恢复到 Jackson 2 的 90% 以上,反序列化恢复到 96% 以上。
高并发、低延迟的服务升级 Jackson 3 后一定要配 threadLocalPool,否则序列化性能可能掉三成。
八、从 Boot 2 到 Jackson 3,分两步走
中间隔着 Spring Boot 3 的 Jakarta 迁移,别想一步到位。
第一步:Boot 2 → Boot 3
和 Jackson 无关,但绕不开:
javax.*→jakarta.*(全局替换)- Java 升到 17+
- 第三方依赖升到 Jakarta 兼容版本
第二步:Boot 3 → Boot 4
三条路线:
路线 A:直接迁 Jackson 3(新项目 / 小项目)
- 全局替换
com.fasterxml.jackson→tools.jackson(排除 annotations) new ObjectMapper()→JsonMapper.builder().build()- 检查默认值变化,调整测试
- 替换异常捕获
IOException→JacksonException - 配 threadLocalPool 优化性能
- 更新注解(
@JsonComponent→@JacksonComponent) - 调整 YAML 配置前缀(加
json:层级)
路线 B:迁 Jackson 3 + 兼容模式(存量项目)
- 升级到 Spring Boot 4,引入 Jackson 3
- 加
spring.jackson.use-jackson2-defaults=true,保持旧行为 - 逐步替换包名和 API
- 移除兼容配置,逐个确认默认值变化的影响
路线 C:暂时用回 Jackson 2(最保守)
- 升级到 Spring Boot 4
- 引入
spring-boot-jackson2,继续用 Jackson 2 - 制定 Jackson 3 迁移计划,分模块推进
九、几个值得用的新特性
除了改名和改默认值,Jackson 3 也有一些新东西。
ObjectNode 支持链式调用
// Jackson 2.x:set 返回旧值,不能链式
node.put("name", "张三");
node.put("age", 25);
// Jackson 3.x:set 返回自身
node.set("name", StringNode.valueOf("张三"))
.set("age", IntNode.valueOf(25));
支持 java.nio.file.Path
// Jackson 2.x
User user = mapper.readValue(new File("user.json"), User.class);
// Jackson 3.x
User user = mapper.readValue(Path.of("user.json"), User.class);
自动探测 sealed classes
// Jackson 2.x:多态必须加 @JsonSubTypes
@JsonTypeInfo(use = Id.NAME)
@JsonSubTypes({
@Type(value = Dog.class, name = "dog"),
@Type(value = Cat.class, name = "cat")
})
public abstract class Animal {}
// Jackson 3.x:sealed class 自动识别,不用 @JsonSubTypes
public sealed interface Animal permits Dog, Cat {}
Jackson 3 通过反射直接拿到 permits 列表,省心。
序列化器缓存有上限了
Jackson 2 的序列化器缓存无上限,类多的时候可能 OOM。Jackson 3 加了缓存大小上限,默认 2000 个,可配置调整。不少生产环境的 OOM 就是 Jackson 缓存撑爆的。
最后
变化确实大:包名换了、API 改了、默认值翻了、异常模型重构了、性能特性也变了。但站在 Jackson 团队的角度,这些改动都是攒了十几年该还的债。一个库用了 12 年,底层架构不动才是问题。
不是 Jackson 变了,是 Java 变了,Jackson 终于追上来了。
你的项目用上Spring Boot 4了吗?是不是你们的技术负责人以稳定为主,不愿意去升级?他们的舞台已经落幕了,不升级会不会影响你将来在舞台上的表演?说说你的看法哈。