Ohhnews

分类导航

$ cd ..
Baeldung原文

使用Jackson库实现多参数构造函数的JSON反序列化

#jackson#json反序列化#java#数据处理#api开发

[LOADING...]

1. 简介

在本教程中,我们将学习如何使用 Jackson 将 JSON 反序列化为使用多参数构造函数的 Java 对象。

默认情况下,Jackson 要求提供一个不带任何参数的默认构造函数。 字段通过 setter 方法或反射进行设置。如果我们希望 Jackson 使用非默认构造函数,则需要使用 @JsonCreator 注解来标注该构造函数。此注解可以应用于记录(Record)和枚举(Enum)的构造函数以及静态工厂方法。在本教程中,我们将探讨所有这些情况。

我们还将了解 Jackson 提供的各种选项,以减少反序列化所需的注解数量。

2. 设置

2.1. Maven 依赖

除了基础的 Jackson 依赖 外,我们还需要 Jackson 的参数名称模块

$ xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.17.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-parameter-names</artifactId>
    <version>2.17.2</version>
</dependency>

2.2. Java 类

在整个教程中,我们将使用 Ticket 类:

$ java
public class Ticket {
    @JsonProperty("event")
    private String eventName;
    private String guest;
    private final Currency currency;
    private final int price;

    public Ticket() {
        this.price = 0;
        this.currency = Currency.EUR;
    }

    public void setGuest(String guest) {
        this.guest = guest;
    }
    
    // 所有属性的 getter 方法
}

请注意,我们仅为 guest 属性定义了 setter 方法,同时提供了所有属性的 getter。这里我们特意省略了其他三个属性的 setter,以演示反序列化的行为。此外,我们在 eventName 属性上使用了 @JsonProperty,以展示 Jackson 提供的序列化字段的不同方式。

以下是 Currency 枚举:

$ java
public enum Currency {
    EUR("Euro", "cent"),
    GBP("Pound sterling", "penny"),
    CHF("Swiss franc", "Rappen");

    private String fullName;
    private String fractionalUnit;

    Currency(String fullName, String fractionalUnit) {
        this.fullName = fullName;
        this.fractionalUnit = fractionalUnit;
    }
}

2.3. 待反序列化的 JSON

以下是我们将在示例中反序列化的 JSON:

$ cat
{
  "event": "Devoxx",
  "guest": "Maria Monroe",
  "currency": "EUR",
  "price": 50
}

3. 默认反序列化

我们需要定义一个默认的无参构造函数,因为类中包含一个 final 属性。如果我们只定义一个接受 currencyprice 作为参数的构造函数,Jackson 将无法反序列化该对象:

$ java
public Ticket(Currency currency, int price) {
    this.price = price;
    this.currency = currency;
}

Jackson 将 抛出异常

$ bash
com.fasterxml.jackson.databind.exc.MismatchedInputException: 
  Cannot construct instance of `com.baeldung.jackson.multiparameterconstructor.Ticket` 
  (no Creators, like default constructor, exist): cannot deserialize from Object value

4. 使用 @JsonCreator 进行反序列化

要指定反序列化时应使用哪个构造函数,我们可以使用 @JsonCreator 注解。

4.1. 定义带有 @JsonCreator@JsonProperty 的构造函数

如果我们想使用双参数构造函数,需要使用 @JsonCreator 标注它,并使用 @JsonProperty 标注参数:

$ java
@JsonCreator
public Ticket(@JsonProperty("currency") Currency currency, @JsonProperty("price") int price) {
    this.price = price;
    this.currency = currency;
}

Jackson 将按以下方式反序列化 JSON:

  • currencyprice 这两个属性在构造函数中设置。
  • guest 属性通过其 setter 方法设置。
  • eventName 属性没有 setter 方法,它是通过反射设置的。

如果属性没有 setter 方法,Jackson 会根据名称通过反射设置该属性。 如果 Java 属性名与 JSON 字段名不同,我们可以使用 @JsonProperty 注解来定义 JSON 字段名。在我们的示例中,我们用 @JsonProperty("event") 标注了 eventName 属性,以表明 JSON 字段名为 event,而 Java 属性名为 eventName

我们只能用 @JsonCreator 标注一个构造函数。如果我们标注了第二个构造函数:

$ java
@JsonCreator
public Ticket(@JsonProperty("currency") Currency currency, @JsonProperty("price") int price, @JsonProperty("guest") String guest) {
    this.price = price;
    this.currency = currency;
    this.guest = guest;
}

Jackson 将抛出异常:

$ bash
com.fasterxml.jackson.databind.JsonMappingException: 
  com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
    Conflicting property-based creators: 
    already had explicitly marked creator [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (2 args), 
    annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}, 
    encountered another: [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (3 args), 
    annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}

4.2. 不带 @JsonProperty 的构造函数

Jackson 使用反射将 JSON 字段名映射到 Java 类属性。这适用于类属性,但不适用于方法参数名。因此,如果我们定义的构造函数没有 @JsonProperty 注解:

$ java
@JsonCreator
public Ticket(Currency currency, int price, String guest) {
    this.price = price;
    this.currency = currency;
    this.guest = guest;
}

我们将得到一个异常:

$ bash
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
  Invalid type definition for type `com.baeldung.jackson.multiparameterconstructor.Ticket`: 
  Argument #0 of constructor [constructor for `com.baeldung.jackson.multiparameterconstructor.Ticket` (2 args), 
  annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)} 
  has no property name (and is not Injectable): can not use as property-based Creator

Java 在运行时不会保留方法参数名称,因此我们需要用 @JsonProperty 标注参数,以指定应映射到每个参数的 JSON 字段名。

如果我们想避免使用 @JsonProperty 标注参数,可以注册 ParameterNamesModule 并将 parameters 标志添加到编译器中。

首先,添加 Maven 依赖

$ xml
<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-parameter-names</artifactId>
    <version>2.21.1</version>
</dependency>

然后,将 ParameterNamesModule 注册到对象映射器(Object Mapper)中:

$ java
ObjectMapper mapper = JsonMapper.builder()
  .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
  .addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
  .build();

并将 parameters 标志添加到编译器配置中:

$ xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

4.3. 使用 ConstructorDetector 进行配置

我们仍然需要添加 @JsonCreator 注解来标记想要用于反序列化的构造函数。

从 Jackson 2.12 开始,我们可以注册一个 ConstructorDetector 来指定反序列化时应使用的构造函数,而无需使用 @JsonCreator 进行标注:

$ java
ObjectMapper mapper = JsonMapper.builder()
  .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
  .addModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES))
  .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
  .build();

值得注意的是,如果我们配置了 Jackson 自动检测构造函数,同时又用 @JsonCreator 标注了多个构造函数中的一个,那么被标注的构造函数将优先于其他被检测到的构造函数。

5. Records

Jackson 可以使用默认提供的规范构造函数来反序列化 Java Record。考虑以下 Record:

$ java
public record Guest(@JsonProperty("firstname") String firstname, @JsonProperty("surname") String surname) {}

以及以下 JSON:

$ cat
{
  "firstname": "Maria",
  "surname": "Monroe"
}

Jackson 可以在不需要无参构造函数的情况下将 JSON 反序列化为 Java Record。 如果我们注册了 ParameterNamesModule 并使用 parameters 标志编译代码,则无需使用 @JsonProperty 标注 Record 组件:

$ java
public record Guest(String firstname, String surname) {}

在某些情况下,我们可能希望自定义规范构造函数:

$ java
public Guest(String firstname, String surname) {
    this.firstname = firstname;
    this.surname = surname;
    // 一些验证逻辑
}

同样,我们不需要用 @JsonCreator 标注此构造函数,因为 Jackson 默认会使用规范构造函数。

我们需要使用 @JsonCreator 注解的一种情况是添加静态工厂方法时:

$ java
@JsonCreator
public static Guest fromJson(String firstname, String surname) {
    // 一些验证逻辑
    return new Guest(firstname, surname);
}

与使用构造函数不同,静态工厂方法可以有额外的参数:

$ java
@JsonCreator
public static Guest fromJson(String firstname, String surname, int id) {
    // 一些验证逻辑
    return new Guest(firstname, surname);
}

而非规范构造函数即使被标注了 @JsonCreator,也不会被使用:

$ java
@JsonCreator
public Guest(String firstname, String surname, int id) {
    this(firstname, surname);
    // 一些验证逻辑
}

Jackson 会忽略此构造函数,转而使用规范构造函数。

6. 枚举 (Enums)

@JsonCreator 也可用于反序列化枚举。默认情况下,Jackson 使用枚举的名称来 反序列化枚举

我们可以通过使用 @JsonFormat 标注枚举来更改默认行为:

$ java
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum Currency {
    EUR("Euro", "cent"),
    GBP("Pound sterling", "penny"),
    CHF("Swiss franc", "Rappen");

    private String fullName;
    private String fractionalUnit;

    Currency(String fullName, String fractionalUnit) {
        this.fullName = fullName;
        this.fractionalUnit = fractionalUnit;
    }
    
    // 所有属性的 getter 方法
}

枚举值 EUR 将被序列化为以下 JSON:

$ cat
{
  "fullName": "Euro",
  "fractionalUnit": "cent"
}

然而,反序列化会因以下异常而失败:

$ bash
com.fasterxml.jackson.databind.exc.MismatchedInputException: 
  Cannot deserialize value of type `com.baeldung.jackson.multiparameterconstructor.Currency` 
  from Object value (token `JsonToken.START_OBJECT`)

这是因为 Jackson 尝试基于枚举的名称(即 EUR)进行反序列化。 我们可能认为用 @JsonCreator 标注构造函数能解决问题:

$ java
@JsonCreator
Currency(String fullName, String fractionalUnit) {
    this.fullName = fullName;
    this.fractionalUnit = fractionalUnit;
}

这行不通,因为枚举构造函数是私有的,Jackson 无法使用。 解决方案是定义一个静态工厂方法并用 @JsonCreator 标注它:

$ java
@JsonCreator
public static Currency fromJsonString(String fullName, String fractionalUnit) {
    for (Currency c : Currency.values()) {
        if (c.fullName.equalsIgnoreCase(fullName) && c.fractionalUnit.equalsIgnoreCase(fractionalUnit)) {
            return c;
        }
    }
    throw new IllegalArgumentException("Unknown currency: " + fullName + " " + fractionalUnit);
}

7. 结论

在本文中,我们学习了如何使用多参数构造函数将 JSON 反序列化为 Java 对象。我们了解了如何在 Java 类、枚举和 Record 中使用 @JsonCreator。此外,我们还了解到参数名称模块和构造函数检测器设置有助于减少所需的注解数量。

一如既往,本文中的代码可在 GitHub 上获取。