【网上的都不靠谱?还是得改源码】用Javasisst的字节码插桩技术,彻底解决Gson转Map时,Int变成double问题

1,977 阅读4分钟

一、探究原由

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

  • 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的核心代码:
@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对象,这也就是问题的根源。

二、网上的“半”解决方案

网罗了网上的解决方案,无非就以下几种。

2.1 自定义一个适配TreeMap的TypeAdapter

重新添加一个自定义的TypeAdapter,解决实现Json串转Map。注意它解决了Json串转Map问题,但是未能解决Json串转对象问题

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();

2.2 自定义一个适配指定类的TypeAdapter

重新添加一个自定义的TypeAdapter,解决实现Json串转指定对象。注意它仅仅解决了Json串转指定对象问题,但是未能解决Json串转Map问题

并且经测试,以下代码使用时会报错,原因不明……

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进行字节码插桩!

3.1 Javasisst入门

简单入门使用,看这篇简书就好:www.jianshu.com/p/b9b3ff0e1…

简单归纳就是,读取原class文件,修改类、方法、属性等,然后重新生成class字节码文件

我们使用一个叫做insertAt()的方法,按行号来插入代码段(如果行号表包含在类文件中),将编译后的代码插入到指定行号位置。

注意:行号是源文件jar包中相关位置的行号。

3.2 方法步骤

下载好gson-2.7.jargson-2.7-sources.jar这两个文件。 然后从gson-2.7-sources.jar中找到要修改的相关类的具体行号位置:

com.google.gson.internal.bind.ObjectTypeAdapter

注意:行号应是78,而不是79!

然后书写插桩代码:

/**
 * @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包的相关位置即可。

到这儿就结束了!

你以为还很复杂?