探究原由

首先申明一下,我们要解决的问题有两个:

  • Json串转Map时,int变double问题
  • Json串转对象时,对象属性中的Map,int变double问题

然后,我们来了解一下,Gson实现Json反序列化的源码:

  1. Gson内部会维护一个类型适配器集合,里面大概有十多个内置的TypeAdapter。涵盖了八大基本类型的TypeAdapter,并且还有一个ObjectTypeAdapter。同时Gson支持自定义TypeAdapter,可以在内置的适配器集合中添加新的类型适配器
  2. 在具体的Json数据反序列化时,首先会根据传入的对象Class,来获取对应的TypeAdapter,然后根据获取的TypeAdapter实现Json到对象的转换。
  3. 因此,在反序列化时,int(Integer)、string等对象属性能匹配到对应的TypeAdapter,进行正确的反序列化。但是如果对象属性为Map时(或者本身就是Json串转Map),将默认由ObjectTypeAdapter类来完成数据的解析。
  4. 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
    38
    @Override 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:
    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
16
Gson gson = new GsonBuilder().registerTypeAdapter(new TypeToken<TreeMap<String, Object>>(){}.getType(), 
new JsonDeserializer<TreeMap<String, Object>>() {
@Override
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
79
public final class MyTypeAdapter extends TypeAdapter<Object> {

public static final FACTORY(Class clazz) {
@Override 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;
}

@Override 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();
}
}

@SuppressWarnings("unchecked")
@Override 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.jargson-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包的相关位置即可。

到这儿就结束了!

你以为还很复杂?