彻底解决 Gson 将 int 转换为 double 的问题

1、现状及原因

目前网络上的资料中有两种解决这个问题的办法。

第一种:修改源码。

第二种:添加指定类型的自定义解密工具。

所有能找到的资料中,都对第一种进行了忽略,认为修改源码改动太大,转而使用了第二种方式。

不论是注册Map的解析器,还是使用自定义的Bean对象,本质都是指定类型解析,也就是第二种,完全无法解决以下问题。

gson.fromJson(s,new TypeToken<List<Map<String,Object>>>(){}.getType());

在这段代码中,你可以测试网络中找到的其他解决方式,你会发现所有设置都失效了。

原因是,当你使用 Class 对象作为解析类型时,Gson 会转入你自定义的解析器,而使用 TypeToken 时,Gson 无法将传入的参数与绑定的 TypeToken 对应,最终使用默认解析器。

2、最终解决办法

由上面的讲解过程,你会明白,第二种方法无法实现,只有修改源码才能达到最终目标。

大多数人没有意识到,修改源码不一定要重新编译,反射一样可以实现。

Gson 中默认处理数值转换的类为 com.google.gson.internal.bind.ObjectTypeAdapter,我们只需要在内存中将其替换即可。

ObjectTypeAdapter 在创建的 Gson 对象中是不存在的,其使用了内部的工厂对象(FACTORY)动态创建。

而 FACTORY 会在 Gson 的构造函数中加入 factories 对象中。

最终,factories 对象通过 Collections 的方法变为不可变列表后保存为成功变量。

找来找去,发现,我们只需要把 Gson 实例中的 factories 对象内部的工厂对象取代即可。

3、实现过程

第一步,创建自定义的 ObjectTypeAdapter 对象,并实现其工厂方法。

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

    private final Gson gson;

    private MapTypeAdapter(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<>();
                in.beginArray();
                while (in.hasNext()) {
                    list.add(read(in));
                }
                in.endArray();
                return list;

            case BEGIN_OBJECT:
                Map<String, Object> map = new LinkedTreeMap<>();
                in.beginObject();
                while (in.hasNext()) {
                    map.put(in.nextName(), read(in));
                }
                in.endObject();
                return map;
            case STRING:
                return in.nextString();
            case NUMBER:
                String s = in.nextString();
                if (s.contains(".")) {
                    return Double.valueOf(s);
                } else {
                    try {
                        return Integer.valueOf(s);
                    } catch (Exception e) {
                        return Long.valueOf(s);
                    }
                }
            case BOOLEAN:
                return in.nextBoolean();
            case NULL:
                in.nextNull();
                return null;
            default:
                throw new IllegalStateException();
        }
    }

    @Override
    public void write(JsonWriter out, Object value) throws IOException {
        if (value == null) {
            out.nullValue();
            return;
        }
        //noinspection unchecked
        TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass());
        if (typeAdapter instanceof ObjectTypeAdapter) {
            out.beginObject();
            out.endObject();
            return;
        }
        typeAdapter.write(out, value);
    }
}

其实改动部分只有对 NUMBER 分支的细化,将原始数据是否包含小数点来作为其是否为整数与小数的依据。 我认为原始数据中字面值为 1.0 的数是小数,而字面值为 1 的数为整数。你也可以有自己的实现方式。

第二步,使用自定义工厂方法取代 Gson 实例中的工厂方法。

    public Gson getGson() {
        Gson gson = new GsonBuilder().create();
        try {
            Field factories = Gson.class.getDeclaredField("factories");
            factories.setAccessible(true);
            Object o = factories.get(gson);
            Class<?>[] declaredClasses = Collections.class.getDeclaredClasses();
            for (Class c : declaredClasses) {
                if ("java.util.Collections$UnmodifiableList".equals(c.getName())) {
                    Field listField = c.getDeclaredField("list");
                    listField.setAccessible(true);
                    List<TypeAdapterFactory> list = (List<TypeAdapterFactory>) listField.get(o);
                    int i = list.indexOf(ObjectTypeAdapter.FACTORY);
                    list.set(i, MapTypeAdapter.FACTORY);
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return gson;
    }

代码中,首先获得 gson 实例的 factories 属性,将属性设置为 public 访问权限,然后获得其属性 o。

因为在 gson 的创建过程中,factories 通过 Collections 的方法变为了不可修改对象,所以我们需要将其真实属性获得才能进行修改。

通过 Collections 的字节码对象获得其声明的所有内部类,遍历内部类获得 UnmodifiableList 类的字节码对象,最后获得其进行包装之前的真实列表数据 listField,并设置其访问权限为 public。

最终获得了真实的 factories 列表 list。

最后一步,得到 ObjectTypeAdapter.FACTORY 在列表中的位置,并用自定义的工厂对象取代之。需要注意的是,必须要将工厂对象同位置替换,因为解析优先级是和列表中的位置有关的。

大功告成。

4、测试

测试代码

    @Test
    public void fromJson2() {
        String s = "{\"Integer\":123,\"Float\":123.0,\"String\":\"abc\",\"Boolean\":true,\"Double\":123.0}";
        Gson gson1 = new GsonBuilder().create();
        Gson gson2 = getGson();
        System.out.println("gson1: "+gson1.fromJson(s,new TypeToken<Map<String,Object>>(){}.getType()));
        System.out.println("gson2: "+gson2.fromJson(s,new TypeToken<Map<String,Object>>(){}.getType()));
    }

测试结果

gson1: {Integer=123.0, Float=123.0, String=abc, Boolean=true, Double=123.0}
gson2: {Integer=123, Float=123.0, String=abc, Boolean=true, Double=123.0}

Process finished with exit code 0

 

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值