探究原由
首先申明一下,我们要解决的问题有两个:
- Json串转Map时,int变double问题
- Json串转对象时,对象属性中的Map,int变double问题
然后,我们来了解一下,Gson实现Json反序列化的源码:
- Gson内部会维护一个
类型适配器
集合,里面大概有十多个内置的TypeAdapter。涵盖了八大基本类型的TypeAdapter,并且还有一个ObjectTypeAdapter。同时Gson支持自定义TypeAdapter,可以在内置的适配器集合中添加新的类型适配器
- 在具体的Json数据反序列化时,首先会根据传入的对象Class,来获取对应的TypeAdapter,然后根据获取的TypeAdapter实现Json到对象的转换。
- 因此,在反序列化时,int(Integer)、string等对象属性能匹配到对应的TypeAdapter,进行正确的反序列化。但是如果对象属性为Map时(或者本身就是Json串转Map),将默认由ObjectTypeAdapter类来完成数据的解析。
- ObjectTypeAdapter的核心代码:
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
37
38public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch (token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<Object>();
in.beginArray();
while (in.hasNext()) {
list.add(read(in));
}
in.endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap<String, Object>();
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map;
case STRING:
return in.nextString();
case NUMBER:
return in.nextDouble();
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new IllegalStateException();
}
}
上面可以看到,针对所有的Number类型,均使用了nextDouble()
来返回了一个Double对象,这也就是问题的根源。
网上的“半”解决方案
网罗了网上的解决方案,无非就以下几种。
自定义一个适配TreeMap的TypeAdapter
重新添加一个自定义的TypeAdapter,解决实现Json串转Map。注意:它解决了Json串转Map问题,但是未能解决Json串转对象问题
!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Gson gson = new GsonBuilder().registerTypeAdapter(new TypeToken<TreeMap<String, Object>>(){}.getType(),
new JsonDeserializer<TreeMap<String, Object>>() {
public TreeMap<String, Object> deserialize(
JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
TreeMap<String, Object> treeMap = new TreeMap<>();
JsonObject jsonObject = json.getAsJsonObject();
Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
for (Map.Entry<String, JsonElement> entry : entrySet) {
treeMap.put(entry.getKey(), entry.getValue());
}
return treeMap;
}
}).create();
自定义一个适配指定类的TypeAdapter
重新添加一个自定义的TypeAdapter,解决实现Json串转指定对象。注意:它仅仅解决了Json串转指定对象问题,但是未能解决Json串转Map问题
!
并且经测试,以下代码使用时会报错,原因不明……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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79public final class MyTypeAdapter extends TypeAdapter<Object> {
public static final FACTORY(Class clazz) {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() == clazz) {
return (TypeAdapter<T>) new ObjectTypeAdapter(gson);
}
return null;
}
};
private final Gson gson;
ObjectTypeAdapter(Gson gson) {
this.gson = gson;
}
public Object read(JsonReader in) throws IOException {
JsonToken token = in.peek();
switch (token) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<Object>();
in.beginArray();
while (in.hasNext()) {
list.add(read(in));
}
in.endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedTreeMap<String, Object>();
in.beginObject();
while (in.hasNext()) {
map.put(in.nextName(), read(in));
}
in.endObject();
return map;
case STRING:
return in.nextString();
case NUMBER:
Double tmp = in.nextDouble();
if (tmp.longValue() = tmp.doubleValue)
return Long.valueOf(tmp.longValue());
return tmp;
case BOOLEAN:
return in.nextBoolean();
case NULL:
in.nextNull();
return null;
default:
throw new IllegalStateException();
}
}
"unchecked") (
public void write(JsonWriter out, Object value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass());
if (typeAdapter instanceof ObjectTypeAdapter) {
out.beginObject();
out.endObject();
return;
}
typeAdapter.write(out, value);
}
}
//使用
Gson gson = new GsonBuilder().registerTypeAdapterFactory(MyTypeAdaptor.FACTORY(Person.class)).create();
彻底的解决方案
我们知道,还有一种彻底的解决方案,那就是修改源代码。但是修改源代码是一件痛苦的事情:
- 需要解决各种依赖环境问题
- 有些没有源码包的还需要反编译成Java文件
- 重新打包,重新打包有时不那么顺利,可能出现各种JavaDoc问题之类的……
- 各种麻烦,谁用谁知道……
因此,我们尝试用Javasisst进行字节码插桩!
Javasisst入门
简单入门使用,看这篇简书就好:https://www.jianshu.com/p/b9b3ff0e1bf8
简单归纳就是,读取原class文件,修改类、方法、属性等,然后重新生成class字节码文件
我们使用一个叫做insertAt()
的方法,按行号来插入代码段(如果行号表包含在类文件中),将编译后的代码插入到指定行号位置。
注意:行号是源文件jar包中相关位置的行号。
方法步骤
下载好gson-2.7.jar
、gson-2.7-sources.jar
这两个文件。
然后从gson-2.7-sources.jar
中找到要修改的相关类的具体行号位置:
com.google.gson.internal.bind.ObjectTypeAdapter
注意:行号应是78,而不是79!
然后书写插桩代码: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
37
38
39
40
41/**
* @Description: javasisst插桩
* @Author localhost01.cn
* @Date: Created in 22:29 2019-03-27
*/
public class Main {
public static void main(String[] args) throws Exception {
// 1.得到反编译的池
ClassPool pool = ClassPool.getDefault();
// 2.导入需要用到的包
pool.importPackage("com.google.gson.stream");
pool.importPackage("java.io");
pool.importPackage("java.util");
pool.importPackage("java.lang");
pool.importPackage("com.google.gson.internal");
// 3.取得需要反编译的jar文件
pool.insertClassPath("D:\\gson-2.7.jar");
// 4.取得需要反编译要修改的类,注意是全路径
CtClass cc = pool.get("com.google.gson.internal.bind.ObjectTypeAdapter");
// 5.取得需要修改的方法
CtMethod method = cc.getDeclaredMethod("read");
method.insertAt(78, "if (true){\n"
+ " Double tmp = Double.valueOf(in.nextDouble());\n"
+ " if (tmp.longValue() == tmp.doubleValue()) {\n"
+ " return Long.valueOf( tmp.longValue());\n"
+ " } else {\n"
+ " return tmp;\n"
+ " }\n"
+ "}");
// 6.写入
cc.writeFile(); //这儿也可以传入一个参数,指定新class要输出的位置
System.out.println("alright!");
}
}
OK,把生成的ObjectTypeAdapter.class文件替换到gson-2.7.jar
包的相关位置即可。
到这儿就结束了!
你以为还很复杂?
更多文章,请关注:开猿笔记