前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从Gson 的一个著名Bug说起

从Gson 的一个著名Bug说起

作者头像
Antony
发布2021-07-14 17:38:48
1.7K0
发布2021-07-14 17:38:48
举报

Number类型盲转Double

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

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

此处假装有一个案例

问题产生的原因

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

代码语言:javascript
复制
代码语言:javascript
复制
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();
        }
    }
代码语言:javascript
复制

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

WorkAround

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

代码语言:javascript
复制
代码语言:javascript
复制

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);
            }
        }
    }
}
代码语言:javascript
复制
<string< span=""></string<>

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

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

代码语言:javascript
复制
new GsonBuilder()
 .registerTypeAdapterFactory(CObjectTypeAdaptor.FACTORY)
                .create();

自定义Bean的类型转换器

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

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

代码语言:javascript
复制
代码语言:javascript
复制
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

@Data
@NoArgsConstructor
public class MockRecord {
    String className;
    String methodName;
    Object[] args;
    Object returning;
}
代码语言:javascript
复制

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

代码语言:javascript
复制
代码语言:javascript
复制
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;
    }

}
代码语言:javascript
复制

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

又出幺蛾子了...

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

xml中代码

代码语言:javascript
复制
select count(1) as count, sum(b) as sum from table group by type

dao中返回

代码语言:javascript
复制
List<Map<String, Object>> getTotal(Map);

service中代码取值:

代码语言:javascript
复制
BigDecimal  rowCount = (BigDecimal)numMap.get(0).get("COUNT");

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

这是咋回事?又咋搞呢?

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 软件测试那些事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • WorkAround
  • 自定义Bean的类型转换器
  • 又出幺蛾子了...
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档