1. 概述

在这篇文章中,我们将概述Optional类,并解释在使用Jackson处理时可能遇到的问题。随后,我们将介绍一个解决方案,让Jackson将Optional对象视为普通的可空对象。

2. 问题概述

首先,让我们来看看当我们尝试使用Jackson序列化和反序列化Optional时会发生什么。

2.1. Maven依赖

为了使用Jackson,确保我们使用的是最新版本:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.13.3</version>
</dependency>

2.2. 我们的Book对象

然后,创建一个包含一个普通字段和一个Optional字段的Book类:

public class Book {
   String title;
   Optional<String> subTitle;
   
   // getters and setters omitted
}

请注意,Optional不应作为字段使用,这里只是为了说明问题。

2.3. 序列化

现在,实例化一个Book对象:

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));

最后,尝试使用Jackson的ObjectMapper进行序列化:

String result = mapper.writeValueAsString(book);

我们会看到Optional字段的输出不包含其值,而是嵌套了一个JSON对象,其中包含一个名为present的字段:

{"title":"Oliver Twist","subTitle":{"present":true}}

虽然这看起来有些奇怪,但这实际上是我们应该期待的。

在这个例子中,isPresent()Optional类的公共getter。这意味着它会根据是否为空,序列化为truefalse。这是Jackson的默认序列化行为。

如果我们考虑一下,我们想要的是实际的subtitle字段的值被序列化。

2.4. 反序列化

现在,让我们反转之前的操作,尝试将一个对象反序列化到一个Optional中。我们会看到现在我们得到一个JsonMappingException:

@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
    String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }";
    Book result = mapper.readValue(bookJson, Book.class);
}

让我们查看堆栈跟踪:

com.fasterxml.jackson.databind.JsonMappingException:
  Can not construct instance of java.util.Optional:
  no String-argument constructor/factory method to deserialize from String value ('The Parish Boy's Progress')

这种行为是有道理的。基本上,Jackson需要一个可以接受subtitle值作为参数的构造函数。但这与我们的Optional字段不符。

3. 解决方案

我们想要的是Jackson将空的Optional视为null,而将有值的Optional视为表示其值的字段。

幸运的是,这个问题已经有了现成的解决方案。Jackson有一系列模块处理JDK 8数据类型,包括Optional

3.1. Maven依赖和注册

首先,添加最新版本作为Maven依赖:

<dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-jdk8</artifactId>
   <version>2.13.3</version>
</dependency>

现在,我们只需要在ObjectMapper上注册模块:

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());

3.2. 序列化

现在,让我们测试一下。如果再次尝试序列化我们的Book对象,我们会看到现在有一个subtitle,而不是嵌套的JSON:

Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
String serializedBook = mapper.writeValueAsString(book);
 
assertThat(from(serializedBook).getString("subTitle"))
  .isEqualTo("The Parish Boy's Progress");

如果尝试序列化一个空的书,它将被存储为null

book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
 
assertThat(from(serializedBook).getString("subTitle")).isNull();

3.3. 反序列化

现在,让我们重复测试反序列化。如果我们重新读取Book,我们将不再看到JsonMappingException:

Book newBook = mapper.readValue(result, Book.class);
 
assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));

最后,让我们再次测试,这次使用null。我们不会得到JsonMappingException,实际上,我们得到了一个空的Optional:

assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());

4. 总结

我们展示了如何利用JDK 8 Data Types模块来解决这个问题,演示了它如何使Jackson将空的Optional视为null,并将有值的Optional视为一个普通的字段。

这些示例的实现可以在GitHub上找到;这是一个基于Maven的项目,可以直接运行。


« 上一篇: Java 8中Infinite Stream