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

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/localhost01/article/details/88866165

一、探究原由

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

  • 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入门

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

简单归纳就是,读取原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包的相关位置即可。

到这儿就结束了!

你以为还很复杂?

展开阅读全文

中文版技术书是不是不靠谱?

01-11

最进看了本 看起来很吃力.后来对照英文版 看了看,感觉很多很多翻译没到位.rn比如拿刚看的第四章举例.rn[img=https://img-bbs.csdn.net/upload/201901/11/1547189637_788716.jpg][/img]rnrn分析一:rn标题rn中文版:"对象的组合"rn英文版:"Composing Objects"rn看到"组合"二字,我以为是指多个对象组合,讲多个对象的关系. 也是第一次看见 组合 (动词) ,对象(名词,组合这个动作的宾语) 这两个词搭配在一起.rn看了一下英文版"Composing Objects",虽然 Object用的复数,但是composing 应该翻译成构成,或者组成.而且整个章节也讲的是对象内部的东西,不涉及多个对象.rn与设计模式中的"组合模式"也没关系.rn我认为标题应该是"对象的组成".rnrn分析二:rn第一段最后一句rn中文版:本章将介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的,并且在为何这些类时不会无意中破坏类的安全性保证.rn英文版:. This chapter covers patterns for structuring classes that canrnmake it easier to make them thread‐safe and to maintain them without accidentally undermining their safetyrnguarantees.rn"一些组合模式" - "patterns for structuring classes" rn应该翻译成"一些构造类的模式"rn而且中文版的整个句子直接使用英语的句子顺序,但是翻译又不到位,读起来特别费劲. 对于句序完全没有优化,简直就是一个词一个词翻译.rnrnrn分析三rn第二段,第一句rn中文版:在线程安全的程序中,虽然可以将程序的所有状态都保存在共有的静态域中,rn英文版:While it is possible to write a thread‐safe program that stores all its state in public static fields, rn这句话逻辑错了吧?rn应该是"虽然将所有状态都保存在共有的静态域中的方法, 也可以写出一个线程安全的程序, " 这个"可以" 用错了地方.rn 而且用 "状态"这个词 谁能看的"状态"指代码中的什么东西?rn 论坛

不靠谱的treeview节点

10-30

版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明rnrnrn作者:xixirnrn出处:http://blog.csdn.net/slowgrace/archive/2008/10/30/3185834.aspxrnrn前几天看到篇文章说VC++是垃圾,如果非要用,就用VC。原因大概其说是VC++的某些模板库充满bug且效率低下。当时看了,觉得这是OOP编程的问题。用对象来搭建应用的思想很迷人,可是当代码出错的时候,你整不清是自己的错,还是你用的别人的对象的错的时候,还会觉得OOP好么?rnrn想起来不应该有这样的情况,因为对象都有接口属性方法的嘛,如果测试对象的属性、方法都对的,那就是你的错嘛?可是如果问题反过来呢?如果经测试,对象的属性值不对,这是你的代码导致的、还是对象内部的bug?这时候你就傻眼了吧?rnrn我现在就想小哭一场,因为我刚就遇到这么一桩。rnrn话说,我有两棵几乎一模一样的树,我要在树B上找到((与(树A上选定节点)相对应的节点)的父亲节点)的key,用这么多括号是为了防止您看这句话理解上出现歧义。rnrn一开始我这么写:rnrn'下面这种写法会引起runtime error, 也许是太长的引用把VBA绕晕了 rnrn strParentKey = m_tree.Nodes.Item(objNode.Key).Parent.Keyrn之后我只好拆成两截这么写:rnrnSet objRelatedNode = m_tree.Nodes.Item(objNode.Key) rnstrParentKey = objRelatedNode.Parent.KeyrnF5之后过了。rnrn可过了一阵儿,又出错了。在我的测试用例中,实际上不止2棵几乎一模一样的树,而是5棵,我给它们分别发消息,它们都重复上面的动作,其中4棵能够正常通过,唯有一棵不能通过,显示“对象变量或with块变量未设置”。我跟进去,发现objRelatedNode.Parent为nothing,而实际上我肯定的知道这个node的爹不是nothing。靠。它自己都存在,它的爹是nothing,天下有这种事么?哪儿说理去?再说,这树是我一行一行代码自己生成的,都是先生成爹,再add child的,怎么可能它的爹是nothing呢?rnrn我反复地试了几次,发现出问题的树总是位于属性页上压在下面的那个属性卡上的树。貌似没显示在画面上treeview控件就不能正确的返回某node的parent,我用了node.ensurevisible也不灵。rnrn靠,你现在明白我前面说的“如果经测试,对象的属性值不对,这是你的代码导致的、还是对象内部的bug?”那种心情了吧?这就像你遇到一个很坏的压根儿不爱你的人,他抛弃了你,可因为你很喜欢他,所以你虽然隐隐觉得错的是他,可还是不停地在那儿找自己的错,怎么找也找不着,那个郁闷、那个委屈以及之后那个怨恨!!!rnrn最让人吐血的是,我添了如下代码之后,竟然就过了:rnrn' If objRelatedNode.Parent Is Nothing Then rn' Stop rn' objRelatedNode.EnsureVisible rn' objRelatedNode.Expanded = True rn' End Ifrn你看我在里面写了一句stop,可自打我加了这段代码之后,就再也没有stop过,也就是说人家就再也不说自己个儿没爹了!!!rnrn更吐血的是,之后我把ACCESS整个关掉,重新打开,注释掉上述代码,也不再出任何错了。rnrn气死人了。合着刚才费了半天心思猜原因、耽误了瑜伽又耽误了晚饭,都是白跟那儿废功夫啊,这什么编程工具呀?rnrn教训:以后遇到莫名其妙的错,不妨重启应用程序、甚至重启系统试一哈。rnrn唉。缓缓神,吃点水果准备睡觉去了。我觉得代码都这么写,软件可太不靠谱了,说起来这控件还是MS的呢。如果软件巨商的产品都是这样,这软件工业的整体质量是不是都很难有效地保障了呢?唉。歇歇先rnrn 论坛

doubleint

04-02

问题的陈述稍有点长请耐心看完。rnrn把double类型转为int类型时,很多时候要作“舍入“,C++11中有个lrint()函数定义如下:rnrnlong int lrint (double x);rnlong int lrint (float x);rnlong int lrint (long double x);rnrnRound and cast to long integerrnRounds x to an integral value, using the rounding direction specified by fegetround, and returns it as a value of type long int.rnrnhttp://www.cplusplus.com/reference/cmath/lrint/rnrn大家可能和我一样最先想到的是,“四舍五入”,但完全不是。rnrn在http://cpp.sh/,我写了段代码测试lrint()的结果,rnrn[code=c]#include rn#include rn#include rn#include rnint main()rnrn for (int i = -10; i < 10; ++i)rn rn double x = i + 0.5;rn rn printf("lrint(%+f)=%ld\n", x, lrint(x));rn rn return 0;rnrn[/code]rnrn运行结果:rnrnlrint(-9.500000)=-10rnlrint(-8.500000)=-8rnlrint(-7.500000)=-8rnlrint(-6.500000)=-6rnlrint(-5.500000)=-6rnlrint(-4.500000)=-4rnlrint(-3.500000)=-4rnlrint(-2.500000)=-2rnlrint(-1.500000)=-2rnlrint(-0.500000)=0rnlrint(+0.500000)=0rnlrint(+1.500000)=2rnlrint(+2.500000)=2rnlrint(+3.500000)=4rnlrint(+4.500000)=4rnlrint(+5.500000)=6rnlrint(+6.500000)=6rnlrint(+7.500000)=8rnlrint(+8.500000)=8rnlrint(+9.500000)=10rnrn这根本不是我所理解的”四舍五入“。查阅WiKi,https://en.wikipedia.org/wiki/Rounding#Round-to-even_method.rnrnC++11遵循IEEE 754 标准的这种算法,经查这种舍入算法叫:四舍六入五成双。http://baike.baidu.com/view/1245064.htmrnrnmsvs2013中已引入了lrint() 函数,当需要大量使用lrint()函数时,我可以毫不夸的说它会把你的程序托慢6-8倍,比如解码音频文件或音频重采样时。rnrn如何提高程序性能,只能重写,rn四舍五入我常用的宏:#define lrint_n(f) (f>=0?(int32_t)(f+(double)0.5):(int32_t)(f-(double)0.4)) rnrn问题来了:“四舍六入五成双” 的宏该怎么写,转换时要不要考虑int最大、最小范围?请大神指教。rnrnrnrnrnrnrnrnrn 论坛

没有更多推荐了,返回首页