本文介绍通过Fastjson反序列化多态类型时遇到的Bug。其要求@type字段位置必须位于json字符串的最前面
问题复现
Fastjson版本
1 2 3 4 5
| <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.83</version> </dependency>
|
下述MalePeople实现了People接口后,我们对其实例进行序列化、反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.aaron.JsonPathTest;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor;
public class Demo4 { public static void main(String[] args) { test1(); }
public static void test1() { People person1 = new MalePeople("Aaron",18); String jsonStr = JSON.toJSONString(person1, SerializerFeature.WriteClassName); People person2 = JSON.parseObject(jsonStr, People.class); System.out.println("json str:" + jsonStr); System.out.println("person2 :"+person2); } }
interface People { }
@AllArgsConstructor @NoArgsConstructor @Builder @Data class MalePeople implements People { private String name;
private Integer age; }
|
从序列化结果看,@type字段记录了真实类型信息。反序列化时也未出现异常
但如果我们对序列化结果的json字符串进行调整,将 @type键值对 不放在json字符串的最前面。重新测试下
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static void test2() { System.out.println("Test 2 --------------------"); String jsonStr1 = "{\"@type\":\"com.aaron.JsonPathTest.MalePeople\",\"age\":18,\"name\":\"Aaron\"}"; People person1 = JSON.parseObject(jsonStr1, People.class); System.out.println("json str 1:" + jsonStr1); System.out.println("person 1:"+ person1);
System.out.println("--------------------------------------------"); String jsonStr2 = "{\"age\":18,\"@type\":\"com.aaron.JsonPathTest.MalePeople\",\"name\":\"Aaron\"}"; People person2 = JSON.parseObject(jsonStr2, People.class); System.out.println("json str 2:" + jsonStr2); System.out.println("person 2:"+ person2); }
|
Bug出现了,json字符串中@type字段前面的键值对在反序列化时失败了
这里补充说明下,序列化时生成的json字符串,@type字段不是在最前面的么?如代码test1方法中的jsonStr所示。为什么反序列化时拿到的json字符串中@type字段的位置顺序就变了呢?原因在于:MySQL 8.0开始提供了对JSON类型数据的支持。但JSON类型的数据是以无序的键值对形式存储的,即不保留键的顺序!!
解决问题
比较简单的解决办法之一:在反序列化指定反序列化的类信息时,使用具体的实现类。而非接口
1 2 3 4 5 6 7
| public static void test3() { System.out.println("Test 3 --------------------"); String jsonStr2 = "{\"age\":18,\"@type\":\"com.aaron.JsonPathTest.MalePeople\",\"name\":\"Aaron\"}"; People person3 = JSON.parseObject(jsonStr2, MalePeople.class); System.out.println("json str 2:" + jsonStr2); System.out.println("person 3:"+ person3); }
|
效果如下所示。此时json字符串中@type字段前面的键值对在反序列化时成功了
但该方案在很多时候局限性非常大。因为其要求你在反序列化之前必须明确清楚地知道具体类型。既然问题的根源在于反序列化时@type字段在json字符串中的位置不是最前面,那我们是否可以想办法在反序列化前对json字符串进行改写?答案自然是可以的。这里我们通过JsonPath进行实现。首先引入JsonPath依赖
1 2 3 4 5 6
| <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>2.7.0</version> </dependency>
|
利用JsonPath改写json字符串的方法,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package com.aaron.JsonPathTest;
import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import java.util.LinkedHashMap; import java.util.Map;
public class JsonUtil { private static final String TYPE_FIELD = "@type";
public static String moveType2Head(String oldJsonStr) { if (oldJsonStr == null) { return null; }
Map<String, Object> oldMap = JsonPath.read(oldJsonStr, "$"); if (!oldMap.containsKey(TYPE_FIELD)) { return oldJsonStr; }
Map<String, Object> newMap = new LinkedHashMap<>(); String typeValue = (String) oldMap.remove(TYPE_FIELD); newMap.put(TYPE_FIELD, typeValue); newMap.putAll(oldMap); DocumentContext newDocument = JsonPath.parse(newMap); return newDocument.jsonString(); } }
|
现在,我们来进行测试
1 2 3 4 5 6 7 8 9
| public static void test4() { System.out.println("Test 4 --------------------"); String jsonStr2 = "{\"age\":18,\"@type\":\"com.aaron.JsonPathTest.MalePeople\",\"name\":\"Aaron\"}"; String jsonStr3 = JsonUtil.moveType2Head(jsonStr2); People person = JSON.parseObject(jsonStr3, People.class); System.out.println("json str 2:" + jsonStr2); System.out.println("json str 3:" + jsonStr3); System.out.println("person :"+ person); }
|
至此,反序列化成功