专栏首页软件测试那些事从Gson 的一个著名Bug说起

从Gson 的一个著名Bug说起

Number类型盲转Double

Gson是一个源自谷歌的JSON序列化/反序列化框架,出身名门,社区活跃,因此被广泛应用。

不过在项目中使用下来,发现Gson有一个比较坑的问题。也就是 明明是整形的数字,经过Gson转换之后,就变成了Double类型。

此处假装有一个案例

问题产生的原因

对于Map以及各种Bean进行序列化和反序列化时,如果某个字段就会发生此类问题。这是因为这些对象如果未指明类型,默认对应的类型是Object,则通过getAdapter()方法查找到的是ObjectTypeAdapter类型适配器。所以默认情况下是由ObjectTypeAdapter类完成此类数据的解析。来看一下ObjectTypeAdapter中是如何处理的?

public final class ObjectTypeAdapter extends TypeAdapter  // ...
    public Object read(JsonReader in) throws IOException {
        JsonToken token = in.peek();
        switch(token) {
        case BEGIN_ARRAY:
            Listnew ArrayList();
            in.beginArray();

            while(in.hasNext()) {
                list.add(this.read(in));
            }

            in.endArray();
            return list;
        case BEGIN_OBJECT:
            Map, Object> map = new LinkedTreeMap();
            in.beginObject();

            while(in.hasNext()) {
                map.put(in.nextName(), this.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();
        }
    }

在读取一个Object时,如果读到的是NUMBER类型的数据,那么Gson会直接用Double类型来进行处理。因此,Int类型的1就会变成1.0。

WorkAround

一个简单的处理方式就是自定义个CObjectTypeAdapter来代替Gson自带的ObjectTypeAdapter处理Object类型的JSON数据。仿照着ObjectTypeAdapter,对于NUMBER部分的处理稍加细化即可:


import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.internal.bind.ObjectTypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class CObjectTypeAdapter   extends TypeAdapter 
public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
  return type.getRawType()
   == Object.class ? (TypeAdapter<T>) new CObjectTypeAdapter(gson) : null;
            }
        };
        private final Gson gson;

        CObjectTypeAdapter(Gson gson) {
        this.gson = gson;
    }

        public Object read(JsonReader in) throws IOException {
        JsonToken token = in.peek();
        switch(token) {
            case BEGIN_ARRAY:
                Listnew ArrayList();
                in.beginArray();

                while(in.hasNext()) {
                    list.add(this.read(in));
                }

                in.endArray();
                return list;
            case BEGIN_OBJECT:
                Map, Object> map = new LinkedTreeMap();
                in.beginObject();

                while(in.hasNext()) {
                    map.put(in.nextName(), this.read(in));
                }

                in.endObject();
                return map;
            case STRING:
                return in.nextString();
            case NUMBER:
                String numberStr = in.nextString();
    if (numberStr.contains(".") || numberStr.contains("e")
                        || numberStr.contains("E")) {
                    return Double.parseDouble(numberStr);
                }
         if (Long.parseLong(numberStr) <= Integer.MAX_VALUE) {
                    return Integer.parseInt(numberStr);
                }
                return Long.parseLong(numberStr);
            case BOOLEAN:
                return in.nextBoolean();
            case NULL:
                in.nextNull();
                return null;
            default:
                throw new IllegalStateException();
        }
    }

public void write(JsonWriter out, Object value) throws IOException {
        if (value == null) {
            out.nullValue();
        } else {
            TypeAdapterthis.gson.getAdapter(value.getClass());
            if (typeAdapter instanceof ObjectTypeAdapter) {
                out.beginObject();
                out.endObject();
            } else {
                typeAdapter.write(out, value);
            }
        }
    }
}
<string< span=""></string<>

这样,当JSON在做Object类型的序列化时,通过使用CObjectTypeAdapter,而不是原生的ObjectTypeAdapter,来规避这个恼人的画蛇添足的问题。

当然,在使用Gson时,需要先注册这个自定义的类型解析器。

new GsonBuilder()
 .registerTypeAdapterFactory(CObjectTypeAdaptor.FACTORY)
                .create();

自定义Bean的类型转换器

在对Object类型提供了自定义的类型解析器之后,顺手再给自定义的Bean做一个类型解析器,保证数据在序列化和反序列化时可以正确解析。

在进行录制回放时,定义了这样一个Bean用于记录每次外部依赖方法调用。

import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@Data
@NoArgsConstructor
public class MockRecord {
    String className;
    String methodName;
    Object[] args;
    Object returning;
}

其中的入参和返回结果分别是Object数组和Object。具体类型由运行时在切面中动态确定。能不能正确地将这个Bean序列化和反序列化是整个录制/回放方案的基础。为此,可以专门为此来编写一个类型转换器MockRecordTypeAdaptor 。

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class MockRecordTypeAdaptor extends TypeAdapter{

    public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
        @SuppressWarnings("unchecked")
        @Override
        public << span="">T> TypeAdapter<< span="">T> create(Gson gson, TypeToken<< span="">T> type) {
            if (type.getRawType() == MockRecord.class) {
                return (TypeAdapter<< span="">T>) new MockRecordTypeAdaptor(gson);
            }
            return null;
        }
    };
    private final Gson gson;

    MockRecordTypeAdaptor(Gson gson) {
        this.gson = gson;
    }

    @Override
    public void write(JsonWriter out, MockRecord value) throws IOException {
        if (value == null) {
            out.nullValue();
            return;
        }

        out.beginObject();
        out.name("className");
        gson.getAdapter(String.class).write(out, value.getClassName());
        out.name("methodName");
        gson.getAdapter(String.class).write(out, value.getMethodName());
        out.name("args");
        gson.getAdapter(Object[].class).write(out, value.getArgs());
        out.name("returning");
        gson.getAdapter(Object.class).write(out, value.getReturning());
        out.endObject();
    }

    @Override
    public MockRecord read(JsonReader in) throws IOException {
        MockRecord data = new MockRecord();
        Map, Object> dataMap = (Map, Object>) CObjectTypeAdapter.readInternal(in);
        data.setClassName((String)dataMap.get("className"));
        data.setMethodName((String)dataMap.get("methodName"));
        List"args");
       Object [] args= argsList==null ? null: argsList.toArray();
       data.setArgs(args) ;
        data.setReturning(dataMap.get("returning"));
        return data;
    }

}

有了这个类型解析器后,各种List、Map等入参和返回值中的数字类型的解析就不再出问题了。录制回放就可以继续愉快地玩耍了。

又出幺蛾子了...

在Mybatis中返回Map中含有数值类型时,类型为BigDecimal 。如返回记录条数和总和。

xml中代码

select count(1) as count, sum(b) as sum from table group by type

dao中返回

List<Map<String, Object>> getTotal(Map);

service中代码取值:

BigDecimal  rowCount = (BigDecimal)numMap.get(0).get("COUNT");

回放时抛出ava.math.BigDecimal cannot be cast to java.lang.Integer

这是咋回事?又咋搞呢?

文章分享自微信公众号:
软件测试那些事

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:风月同天测试人
原始发表时间:2021-07-03
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 重新认识一个强大的 Gson ,从一个线上 BUG 说起

    不知道你们发现没有,你写完的程序无论当时怎么测试,过一段时间总会出 Bug 。再说一个每天都在发生的例子:在你写完一篇博客后,立即检查的话,总是查不出自己写的错...

    芋道源码
  • 从一个 OpenCV 的 BUG 说起~

    我们上次分享过 YOLO 实现目标检测,但是,当笔者今天再测试时发现里面竟然存在一个有趣的 BUG

    机器视觉CV
  • 从被疯狂吐槽的电商搜索Bug说起

    公众号里,我已经写过很多关于电商技术方面的文章,以及普及电商基本知识的文章(比如:SKU,SPU,QPS,GMV等)。除此之外,我还讨论过电商系统的业务复杂度,...

    业余草
  • 2018-07-14 代码中的人文故事:从一个Java的“Bug”说起

    I like this kind of story , can not type in chinese character using ubuntu . so ...

    Albert陈凯
  • 从HTTP 400 bad request说起 - 一个函数被注释掉后引起的血案

    Function引起的血案:今天我的工作任务是研究为什么用API创建微软Azure云平台的Function时会遇到HTTP 400 Bad Request错误,...

    Jerry Wang
  • 重新认识一个强大的 Gson

    不知道你们发现没有,你写完的程序无论当时怎么测试,过一段时间总会出 Bug 。再说一个每天都在发生的例子:在你写完一篇博客后,立即检查的话,总是查不出自己写的错...

    java思维导图
  • 来,重新认识一个强大的 Gson!

    不知道你们发现没有,你写完的程序无论当时怎么测试,过一段时间总会出 Bug 。再说一个每天都在发生的例子:在你写完一篇博客后,立即检查的话,总是查不出自己写的错...

    Java技术栈
  • 有意思,发现Kotlin一个神奇的bug!

    本文将会通过具体的业务场景,由浅入深的引出Kotlin的一个bug,并告知大家这个bug的神奇之处,接着会带领大家去查找bug出现的原因,最后去规避这个bug。

    程序员小顾
  • 大型项目废弃fastjson迁移至Gson保姆级攻略

    在被大家取关之前,我立下一个“远大的理想”,一定要在这周更新文章。现在看来,flag有用了。。。

    蛮三刀酱
  • 解析JSON数组正常,却在获取数组元素时抛出了类型转换异常

    下面这段代码会抛出类型转换异常(ClassCastException),JVM给出的解释是:不能将Double类型对象转换String类型 (java.lang...

    Java艺术
  • 发现Gson一个小Bug?

    最近使用Gson序列化HashMap实例发现序列化结果为null。这其实不是什么Bug,后面我百度一下发现很多人都发现过这个问题,只是不知道出于什么考虑。

    Java艺术
  • Java怎样高速构造JSON字符串[通俗易懂]

    目标:依据key/value高速构造一个JSON字符串作为參数提交到web REST API服务上。 分别測试里阿里巴巴的FastJson和Google G...

    全栈程序员站长
  • Gson:我爸是 Google

    我叫 Gson,是一款开源的 Java 库,主要用途为序列化 Java 对象为 JSON 字符串,或反序列化 JSON 字符串成 Java 对象。从我的名字上,...

    沉默王二
  • JDK 的这3个bug,你发现了吗?

    如果两个变量中间隔了比较长的其它代码,很可能会导致开发人员将两者混淆,导致逻辑认知错误,从而写出或改出有问题的代码。

    用户5224393
  • NJU-Class-Shedule ALPHA-RELEASE v0.1.0!

    为NJUer的Android平台 Material design 课程表 基于星星课表(mnnyang/ClassSchedule)之上进行设计与重构

    idealclover
  • JDK 的 3 个 bug 啊!

    如果两个变量中间隔了比较长的其它代码,很可能会导致开发人员将两者混淆,导致逻辑认知错误,从而写出或改出有问题的代码。

    Java技术栈
  • 弃坑 FastJson,不香了

    首先抄录一段来自官网的介绍:FastJson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可...

    二哥聊运营工具
  • JDK中几个错误的调用方式

    如果两个变量中间隔了比较长的其它代码,很可能会导致开发人员将两者混淆,导致逻辑认知错误,从而写出或改出有问题的代码。

    Spark学习技巧

扫码关注腾讯云开发者

领取腾讯云代金券