1、描述
最近在使用 Jackson 将 Json 串转换回 Java 对象的时候遇到了 ClassCastException 错误,特此记述。
2、问题复现
问题出现的节点在于属性节点的 JavaType 不明确,比如使用了泛型 和 Object,如下:
1 @Getter 2 @Setter 3 @NoArgsConstructor 4 @AllArgsConstructor 5 private static class JsonResult<T> { 6 private String message; 7 private T result; // 问题出现在这里 8 } 9 10 @Builder 11 @Getter 12 @Setter 13 @NoArgsConstructor 14 @AllArgsConstructor 15 private static class Container { 16 private String key; 17 private String value; 18 }
使用如下的测试的用例
1 @Test 2 void testWrite() throws JsonProcessingException { 3 final ObjectMapper mapper = new ObjectMapper(); 4 mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); 5 final JsonResult<Container> resultBefore = new JsonResult<>(); 6 resultBefore.setMessage("foo"); 7 final Container containerBefore = Container.builder().key("key").value("value").build(); 8 resultBefore.setResult(containerBefore); 9 final String writtenString = mapper.writeValueAsString(resultBefore); 10 //----------------read str as object 11 final JsonResult parsedResult = mapper.readValue(writtenString, JsonResult.class); 12 assert parsedResult != null; // 之后将在这里打断点 13 14 final Container container = (Container) parsedResult.result; 15 assertThat(container).as("not null").isNotNull().extracting(Container::getKey).as("key equal").isEqualTo(containerBefore.key); 16 assertThat(container).extracting(Container::getValue).as("key equal").isEqualTo(containerBefore.value); 17 } 18 }
将会得到错误: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to Container
3、问题解析
让我们在测试中打上断点,看看经由 jackson 反序列化后的对象内容
可以看到这时候 Container 类的内容在这里变成了 hashMap,其实各种原因不难理解,在序列化跟反序列化中,我们给 Jackson 传递的 Class 是 JsonResult,回到相应类的定义,会发现我们使用的是泛型,Jackson 并不知道 rsult 中实际存放的类型,对 Object 也是如此(Object 是所有非原始类型的祖先)
在 ide 的提示里,我们也可以看到这时候 parsedResult 里边的 result 实际上是 Object 类型
4、思路与解决方式
4.1 思路
从上边的分析来,看解决的思路很简单,我们需要告诉 Jackson result 中存放的数据类型。方法有二
4.2 解决方式
4.2.1 改变反序列化时传递的 Class 参数
4.2.1.1 泛型
当对象有泛型参数时候,我们只要构建一个新类型,让它继承原本的类并指定泛型参数即可。在原本的代码中,我们加入。
1 private static class ContainerJsonRsult extends JsonResult<Container>{}
并且改变相应的反序列化语句即可。
final ContainerJsonRsult parsedResult = mapper.readValue(writtenString, ContainerJsonRsult.class);
4.2.1.2 Object
当对象的没有泛型签名时,我们需要构建一个新类型,让他继承原本的类,并让他拥有目标 Containner 类型的同名参数 result
1 @Getter 2 @Setter 3 @NoArgsConstructor 4 @AllArgsConstructor 5 private static class ContainerJsonResult extends JsonResult { 6 private Container result; 7 }
然后修改对应的反序列语句:
1 final ContainerJsonResult parsedResult = mapper.readValue(writtenString, ContainerJsonResult.class);
2、在反序列化中手动传递 result 对应的 Calss 类型
对于存在泛型的类,推荐 4.2.1.1 的解决方式,当 rsult 中指向的是 Object 或者 T 类型时,都可以指定相应的 Class 类进行二次转换:
1 Container container = mapper.convertValue(parsedResult.getResult(), Container.class);
或者
1 JavaType type = mapper.getTypeFactory().constructType(Container.class); 2 Container container = mapper.convertValue(parsedResult.result, type);