介绍
在本文中,我们会对 Optional 类进行一些说明,并且会解释下如果在使用 Optional 类的时候可能在 Jackson 中进行序列化和反序列化的过程中出现的问题。
针对上面的问题,本文会将会介绍在 Jackson 中如何处理 Optional 对象,和如果 Optional 对象可能出现潜在的 Null 的解决方案。
问题概览
首先让我们来看看如果使用 Jackson 来对 Optional 数据类型进行序列化和反序列化中出现的问题。
Maven 依赖
针对 Jackson,我们可以使用最新的版本。
当我们对本文进行更新的时候,jackson-core 的最新版本为 2.17.0。
代码语言:javascript复制<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.3</version>
</dependency>
定义 Book 对象
随后让我们来定义一个 Book 对象,在 Book 对象中,我们有一个使用 Optional 的字段。
当然在这个 Book 对象中,我们还需要添加 Getter 和 Setter 方法,在文章中,我们就省略到这些方法了。
代码语言:javascript复制public class Book {
private String title;
private Optional<String> subTitle;
}
在的对数据对象进行初始化的时候,我们需要注意对 Optional 对象设置值的方式,因为不同的值会影响序列化和反序列化的情况。
序列化
让我们先来实例化 Book 这个对象:
代码语言:javascript复制Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
随后,我们使用 Jackson 的 ObjectMapper 方法来对实例化后的对象进行序列化,我们使用下面的代码来进行序列化:
代码语言:javascript复制String result = mapper.writeValueAsString(book);
从输出的字段中,我们可以看到输出的字符串内容中并没有输出具体的值,而是输出为下面的内容:
代码语言:javascript复制{"title":"Oliver Twist","subTitle":{"present":true}}
尽管上面的输出看起来有点奇怪,但是上面的输出却是正确的情况,因为这个和 Optional 的特性是有关的。
方法 isPresent() Optional 的 public getter 方法,这就意味着在序列化的时候基于我们对象中存储的具体的值,Jackson 将会输出 True 或者 False 。
这是 Jackson 当前正确的输出方式。
但,我们可能考虑在输出的时候输出具体的值,至于怎么输出这个具体的值的方法,我们在后续的解决方案中提出。
反序列化
现在,让我们使用上面的代码来对对象数据进行反序列化,考察使用下面的代码:
代码语言:javascript复制@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
String bookJson = "{ "title": "Oliver Twist", "subTitle": "foo" }";
Book result = mapper.readValue(bookJson, Book.class);
}
当上面的代码运行的时候将会提示下面的错误信息:
代码语言:javascript复制com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.util.Optional` (no Creators, like default constructor, exist): no String-argument constructor/factory method to deserialize from String value ('foo')
at [Source: (String)"{ "title": "Oliver Twist", "subTitle": "foo" }"; line: 1, column: 40] (through reference chain: com.ossez.jackson.optionalwithjackson.Book["subTitle"])
上面的错误信息针对 Jackson 来说是正确的,因为 Jackson 是需要一个构造方法来把 subtitle 参数的值来对 Optional 对象进行数据初始化。
解决方案
我们希望的是 Optional 对象应该把一个空的数据设置为 null,如果不是空的数据,Optional 应该使用值来进行处理。
针对上面的要求,Jackson 已经提供了解决方案,Jackson 针对 JDK8 的新增模块设置了一系列数据类型,这里就包括了 Optional。
Maven 依赖
首先,我们需要使用针对 JDK 8 使用的依赖,这个依赖的名称为:jackson-datatype-jdk8
代码语言:javascript复制<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.17.0</version>
</dependency>
现在,我们需要把上面的依赖注册到 ObjectMapper:
代码语言:javascript复制ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
或者上面的 2 句话也可以简化成 1 句话:
代码语言:javascript复制 ObjectMapper mapper = new ObjectMapper().registerModule(new Jdk8Module());
序列化
现在,让我们来进行测试。
让我们再次使用上面的代码来对 Book 这个对象进行序列化和反序列化,然后我们在对输出的字符串进行查看。
代码语言:javascript复制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");
如果我们尝试序列化一个空的 Book 对象的话,那么 Optional 字段中存储的数据为 null。
代码语言:javascript复制book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle")).isNull();
##反序列化 现在我们来进行反序列化,当我们进行反序列化的时候,我们可以看到上面的代码不再抛出 JsonMappingException 异常。
代码语言:javascript复制Book newBook = mapper.readValue(result, Book.class);
assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));
最后,我们再对上面的测试代码进行测试,从上面的代码代码输出中我们也可以看到没有异常,同时我们还得到了一个空的 Optional 对象。
代码语言:javascript复制assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());
结论
本文对针对 JDK 8 的新特性中提供的一些数据模型进行一些说明。
Jackson 需要注册一个新的 jdk8 数据类型才能对数据进行处理。
因为 Optional 是 JDK 8 中提供的新的数据特性,因此我们对一些新的数据类型我们需要有一些了解。
同时,针对 Jackson 还是有必要保持 JDK 的版本一致性和尽量使用比较高的版本,这样就可以使用更多有关 Jackson 提供的功能。