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。这意味着它会根据是否为空,序列化为true
或false
。这是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的项目,可以直接运行。