Ohhnews

分类导航

$ cd ..
Baeldung原文

Jackson 3 新特性解析:架构重构与迁移指南

#jackson#java#json处理#架构升级#版本迁移

[LOADING...]

1. 概述

十多年来,Jackson 一直是 Java 生态系统中最受欢迎的 JSON 处理库。在 2.x 系列经过多年的增量更新后,Jackson 3.0 的发布引入了重大的架构重构,旨在解决长期存在的技术债务并使其 API 现代化。

在本教程中,我们将探讨 Jackson 3 的关键变化、它们如何影响我们现有的代码库,以及从 2.x 系列迁移所需的步骤。

2. 技术前提与迁移路径

让我们回顾 Jackson 3 的基础性变化,包括最低要求以及为迁移准备项目所需的必要步骤。

2.1. Java 17 基线

Jackson 3.0 将其最低支持的 JDK 版本从 Java 8 提升到 Java 17。这使得 Jackson 能够原生使用现代 Java 语言特性,例如 Java Records 或模式匹配。

在迁移到 Jackson 3.0 之前,我们应确保项目满足此要求。如果不满足,我们必须考虑升级项目中的 Java 版本或继续使用 Jackson 2.x 系列。

2.2. 构件与 GroupId

新的主要 Jackson 版本为其构件引入了全新的 groupId——tools.jackson.core,取代了 com.fasterxml.jackson.core。这一变化允许 Jackson 2.x 和 3.x 在类路径上并行共存,以便逐步迁移。

要安装新版本的库,我们将其添加到 pom.xml 中:

$ xml
<dependency> 
    <groupId>tools.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>3.2.1</version>
</dependency>

或者,如果我们想为另一种格式(如 XML)添加专用映射器,则需要包含其特定模块:

<dependency>
    <groupId>tools.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>3.2.1</version>
</dependency>

对于这些格式特定的模块,组是 tools.jackson.dataformat。还值得注意的是,jackson-annotations 依赖仍然保留在旧的 com.fasterxml.jackson.core 组下。这一例外确保了我们带有 Jackson 注解的核心领域模型在升级时无需修改。

2.3. 包名变更

由于构件 groupId 的变化,基础 Java 包也从 com.fasterxml.jackson 转变为 tools.jackson。在迁移项目时,我们需要更新导入语句:

$ java
// Remove Old Jackson 2.x imports
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
// Use New Jackson 3.x imports
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.JsonNode;

2.4. Android 兼容性

由于 Jackson 3 完全依赖 Java 17,Android 项目必须正确配置以支持这些语言特性。

Android 开发者应将目标 API 级别设为 34(Android 14),或在构建配置中启用核心库脱糖(core library desugaring)以使用 Jackson 3。

2.5. 移除已弃用方法

作为主版本升级的一部分,Jackson 3 移除了 2.x 系列中所有带有 @Deprecated 注解的类、方法和配置。如果我们当前的代码库使用了任何遗留方法,我们必须在迁移前将其替换为 3.x 的替代方案。

现在,让我们继续了解 Jackson 3 中的具体功能变化。

3. 不可变的 ObjectMapper 与构建器模式

在之前的版本中,ObjectMapper 实例是可变的。通过在初始化后修改活动映射器上的配置或注册模块,我们可能会遇到线程安全问题,例如当并发线程同时尝试读取和更改映射器状态时出现竞态条件。

Jackson 3 通过强制其映射器严格不可变来解决此问题。 它移除了 ObjectMapper 上的直接配置方法。 相反,我们使用构建器模式和格式特定的构建器在实例化之前配置映射器。

3.1. 构建 JsonMapper

让我们看看如何使用可用的 API 构造和配置一个 JsonMapper

$ java
JsonMapper mapper = JsonMapper.builder()
  .enable(SerializationFeature.INDENT_OUTPUT)
  .disable(JsonWriteFeature.ESCAPE_NON_ASCII)
  .configure(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, false)
  .build();

在这里,我们为 JSON 格式初始化一个构建器。我们启用美化打印,禁用非 ASCII 字符的转义,并告诉 Jackson 不要将日期序列化为数字时间戳。

Jackson 使用 Java 枚举作为功能开关集来控制解析、序列化和映射行为。 我们可以在 SerializationFeatureDeserializationFeatureMapperFeature 枚举中找到通用配置选项。对于格式或类型特定的需求,Jackson 提供了专用的枚举,例如 JsonWriteFeature(用于 JSON 生成)和 DateTimeFeature(用于日期处理)。

3.2. 复制映射器

如果我们需要现有映射器的略微修改版本,可以使用 rebuild() 方法:

$ java
JsonMapper prettyMapper = mapper.rebuild()
  .enable(SerializationFeature.INDENT_OUTPUT)
  .build();

注意,prettyMapper 和上一个片段中的 mapper 是完全不同的对象。 rebuild() 方法将原始映射器的内部配置复制到新的构建器中,允许我们生成一个带有轻微修改的新实例。由于映射器是不可变的,此操作完全线程安全,并且不会影响原始映射器。

3.3. 支持替代数据格式

如果我们的项目需要不同的数据格式,我们只需切换到相应的格式特定构建器。例如,我们可以使用各自的构建器为 XML 配置一个 XmlMapper,或者为 YAML 配置一个 YAMLMapper

$ java
XmlMapper xmlMapper = XmlMapper.builder()
  .enable(SerializationFeature.INDENT_OUTPUT)
  .build();
YAMLMapper yamlMapper = YAMLMapper.builder()
  .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
  .build();

4. 非受检异常

Jackson 2.x 中最大的可读性问题之一是在序列化和反序列化期间处理受检的 JsonProcessingExceptionIOException。在 Java Streams 或 lambda 表达式中支持它们通常会导致冗长的 try-catch 块或自定义包装工具。

在 Jackson 3 中,JacksonException 取代 JsonProcessingException 成为根异常,并且现在它继承自 RuntimeException

$ java
List<User> users = jsonPayloads.stream()
  .map(json -> mapper.readValue(json, User.class)) // No try-catch needed!
  .toList();

这一变化显著简化了我们的流操作。由于异常是非受检的,编译器不再强制我们立即处理它。我们可以省略 lambda 中杂乱的 try-catch 块,从而得到更简洁的代码。

5. 内置 Java 8+ 特性

历史上,为了正确处理 Java 8 特性,如 java.time 类、java.util.Optional 或构造函数参数名称检测,我们必须显式包含并注册三个独立的模块:jackson-datatype-jsr310jackson-datatype-jdk8jackson-module-parameter-names

由于 Jackson 3 面向 Java 17,这三个模块现已集成到 jackson-databind。我们可以在创建新映射器后立即序列化和反序列化 LocalDateTimeOptional 属性:

$ java
public record Event(String title, Optional<String> description, LocalDateTime eventDate) {}
// ...
Event event = new Event("Tech Talk", Optional.of("Jackson 3 overview"), LocalDateTime.now());
JsonMapper mapper = JsonMapper.builder().build();    
String json = mapper.writeValueAsString(event);
Event deserialized = mapper.readValue(json, Event.class);

这种原生支持显著简化了使用 Java 8+ 类型时的代码库,因为它不需要额外的配置。

6. 配置默认值与行为变更

Jackson 3 更改了若干默认设置,以反映现代开发实践和社区反馈。完整变更列表可在官方文档中找到。我们描述最具影响力的几项。

6.1. FAIL_ON_UNKNOWN_PROPERTIES 默认为 false

在 Jackson 2.x 中,反序列化时遇到未知 JSON 字段默认会抛出 UnrecognizedPropertyException,除非我们在映射器上显式禁用它。

在 Jackson 3.0 中,DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 默认为 falseJackson 会安全地忽略传入 JSON 负载中的未识别字段,除非我们显式将此标志设置为 true。让我们验证一下:

$ java
record User(String name) {}
// ...
JsonMapper mapper = JsonMapper.builder().build();
// The JSON contains "unknownField" which is not present in the User class
String json = "{\"name\":\"Alice\", \"unknownField\":\"ignored_value\"}";
    
// This executes without throwing an exception
User user = mapper.readValue(json, User.class);

在某些情况下,例如严格的 API 验证,可能需要在遇到未知字段时抛出异常。为此,我们在构建器上重新启用该特性:

$ java
JsonMapper strictMapper = JsonMapper.builder()
    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .build();
User strictUser = strictMapper.readValue(json, User.class);

现在这会抛出 UnrecognizedPropertyException

6.2. 快速浮点运算

Jackson 3 默认启用快速浮点解析和写入算法,显著提高性能,在处理大量数值负载时。

在 Jackson 2.x 的后期版本中,我们必须通过配置工厂特性 StreamReadFeature.USE_FAST_DOUBLE_PARSER 来手动激活此行为:

$ java
JsonFactory.builder()
    .enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER)
    .build()

在 Jackson 3 中,我们可以开箱即用地享受这一性能提升。

6.3. 简化的日期和时间格式化

Jackson 3 引入了 DateTimeFeature,将日期和时间配置选项跨标准 JDK 日期、Java 8 java.timeJoda-Time 整合到一个统一的特性集中:

$ java
JsonMapper mapper = JsonMapper.builder()
    .enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
    .build();

现在我们可以跨所有日期类型全局格式化日期。

6.4. 标准 Bean 命名

Jackson 3 将标准 bean 命名作为永久默认行为融入其核心引擎。 由于此行为不再是可选的,Jackson 3 完全移除了 MapperFeature.USE_STD_BEAN_NAMING 配置标志。

现在,我们开箱即用地获得更一致的属性命名,因为我们可以从标准 getter 和 setter 派生属性名。考虑一个带有 getter getTheURL() 的 Java 类:

$ java
class Link {
    private String address;
    public Link(String address) {
        this.address = address;
    }
    public String getTheURL() {
        return address;
    }
}
// ...
String json = mapper.writeValueAsString(new Link("https://baeldung.com"));

请注意,Jackson 从 getter 而不是私有字段派生 JSON 属性名。在此示例中,getter getTheURL() 决定输出键 theURL

$ cat
{"theURL":"https://baeldung.com"}

背后的私有字段 address 不会被考虑。## 7. JsonNode 树模型的增强

JsonNode 树模型对于处理非结构化数据仍然至关重要。在 Jackson 3 中,该 API 经历了若干结构性更新,使遍历 JSON 树更安全、更直观。

首先,节点树现在暴露了专用子类型,例如 ObjectTreeNodeArrayTreeNode。与在泛型 JsonNode 接口上检查节点类型相比,这一转变在处理复杂 JSON 结构时提供了更好的类型安全性和更清晰的意图。

API 还引入了更安全的方法名,以符合 Java 惯例。例如,JsonParser.getText() 已重命名为 getString(),以避免歧义并统一命名方案。

最后,Jackson 3 引入了可选值访问器,例如 intValueOpt()asInt()required()。它们消除了手动 null 检查的需要,并提供了一种更简洁的数据提取方式。

以下示例展示了这三项更新:

$ java
String json = "{\"product\":\"Laptop\",\"price\":1200}";
JsonNode root = mapper.readTree(json);
String product = root.get("product").asString();
OptionalInt price = root.get("price").intValueOpt();

在这段代码中,我们将原始 JSON 字符串解析为树模型。然后使用 get() 方法安全地导航该结构,并使用新的 asString()intValueOpt() 访问器提取类型化值。这种方法绕过了以前版本中经常需要的笨拙类型检查和手动强制转换。

8. 结论

在本文中,我们探讨了 Jackson 3.0 引入的重大架构和功能变更。通过将基线提升到 Java 17,该库摆脱了多年的技术债务,并原生拥抱现代 Java 特性,例如 Records、java.util.Optionaljava.time API,而无需外部模块。

通过构建器模式实现的严格不可变性转变,成功解决了历史性的线程安全问题,而向非受检异常的过渡则极大地提升了开发者的可读性,尤其是在使用现代 Java Streams 时。此外,更智能的配置默认值和树模型增强使日常 JSON 处理更简洁、更直观。

虽然从 2.x 系列迁移需要一些工作,但更快、更安全、更健壮的 API 所带来的长期收益,使这次升级非常值得。

一如既往,所有示例的完整源代码都可以在 GitHub 上获取

本文 《Jackson 3 有什么新特性?》 最初发表于 Baeldung