文档章节

Gson格式转换Integer变为Double类型问题解决

明MikeWoo
 明MikeWoo
发布于 2018/12/28 14:10
字数 2005
阅读 406
收藏 10

问题描述

在前后端分离的开发模式下,前后端交互通常采用JSON格式数据.自然会涉及到json字符串与JAVA对象之间的转换。实现json字符串与Java对象相互转换的工具很多,常用的有Json、Gson、FastJSON、Jackson等。一次测试中,在将返回给前端的json字符串反序列化为自定义的Response对象时,发现原先json中的Integer类型被转化为了Double类型。便于问题描述,对原有json字符串简化,示例如下:

{
	"status": 200,
	"msg": "OK",
	"data": [{
		"id": 1,
		"username": "eric",
		"password": "123456",
		"age": 29,
		"sex": 0,
		"permission": 0,
		"isDel": 0
	}]
}

使用Gson(版本gson-2.8.5)的fromJson方法解析,

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
}

解析后,结果如下:

ResponseData(status=200, msg=OK, data=[{id=1.0, username=eric, password=123456, age=29.0, sex=0.0, permission=0.0, isDel=0.0}])

其中ResponseData类定义如下:

@Data
public class ResponseData implements Serializable {
    // 响应业务状态
    private Integer status;

    // 响应消息
    private String msg;

    // 响应中的数据
    private Object data;
}

发现data字段解析后,原有的Integer类型都转换成了Double类型,而status字段却没有被转换为Double类型。那为什么会出现这种现象呢?

原因分析

跟踪Gson实现json字符串反序列化的源码,在实现具体的Json数据反序列化时,首先会根据传入的对象类型Type获取类型适配器TypeAdapter,然后根据获取的TypeAdapter实现Json值到一个对象的转换。

public <T> T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, JsonSyntaxException {
    boolean isEmpty = true;
    boolean oldLenient = reader.isLenient();
    reader.setLenient(true);
    try {
      reader.peek();
      isEmpty = false;
      TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
      TypeAdapter<T> typeAdapter = getAdapter(typeToken);
      T object = typeAdapter.read(reader);
      return object;
    } catch (EOFException e) {
      // 省略异常处理逻辑
    } finally {
      reader.setLenient(oldLenient);
    }
  }

解析中比较关键的就是根据待解析的类型找到对应的类型适配器TypeAdapter<T>类,如果找到类型适配器不合适,就可能造成解析后的数据出问题。类型适配器TypeAdapter是一个抽象类,主要方法如下:

public abstract class TypeAdapter<T> {

  /**
   * Writes one JSON value (an array, object, string, number, boolean or null)
   * for {@code value}.
   *
   * @param value the Java object to write. May be null.
   */
  public abstract void write(JsonWriter out, T value) throws IOException;

  /**
   * Reads one JSON value (an array, object, string, number, boolean or null)
   * and converts it to a Java object. Returns the converted object.
   *
   * @return the converted Java object. May be null.
   */
  public abstract T read(JsonReader in) throws IOException;
}

解析时,类型适配器TypeAdapter通过read()方法读取Json数据,将其转化为Java对象。那么为什么status字段可以正常转换,而data字段转换确有问题呢?

这是由于在解析status字段时,传入的类型Type是一个Integer类型,在调用getAdapter()方法查找TypeAdapter时,遍历TypeAdapterFactory工厂,能找到一个TypeAdapters.INTEGER_FACTORY工厂,通过这个工厂就可以得到一个适用于解析Integer类型字段的类型适配器。

public <T> TypeAdapter<T> getAdapter(TypeToken<T> type) {
    TypeAdapter<?> cached = typeTokenCache.get(type == null ? NULL_KEY_SURROGATE : type);
    if (cached != null) {
      return (TypeAdapter<T>) cached;
    }

    Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
    boolean requiresThreadLocalCleanup = false;
    if (threadCalls == null) {
      threadCalls = new HashMap<TypeToken<?>, FutureTypeAdapter<?>>();
      calls.set(threadCalls);
      requiresThreadLocalCleanup = true;
    }

    // the key and value type parameters always agree
    FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
    if (ongoingCall != null) {
      return ongoingCall;
    }

    try {
      FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
      threadCalls.put(type, call);

      for (TypeAdapterFactory factory : factories) {
        TypeAdapter<T> candidate = factory.create(this, type);
        if (candidate != null) {
          call.setDelegate(candidate);
          typeTokenCache.put(type, candidate);
          return candidate;
        }
      }
      throw new IllegalArgumentException("GSON (" + GsonBuildConfig.VERSION + ") cannot handle " + type);
    } finally {
      threadCalls.remove(type);

      if (requiresThreadLocalCleanup) {
        calls.remove();
      }
    }
  }

而data字段对应的类型是Object,则通过getAdapter()方法查找到的是ObjectTypeAdapter类型适配器。所以默认情况下是由ObjectTypeAdapter类完成data字段数据的解析。

/**
 * Adapts types whose static type is only 'Object'. Uses getClass() on
 * serialization and a primitive/Map/List on deserialization.
 */
public final class ObjectTypeAdapter 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 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:
      return in.nextDouble();

    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将Java中对应的double、long、int都统一为数值类型NUMBER。

/**
 * A structure, name or value type in a JSON-encoded string.
 *
 * @author Jesse Wilson
 * @since 1.6
 */
public enum JsonToken {

  /**
   * The opening of a JSON array. Written using {@link JsonWriter#beginArray}
   * and read using {@link JsonReader#beginArray}.
   */
  BEGIN_ARRAY,

  /**
   * The closing of a JSON array. Written using {@link JsonWriter#endArray}
   * and read using {@link JsonReader#endArray}.
   */
  END_ARRAY,

  /**
   * The opening of a JSON object. Written using {@link JsonWriter#beginObject}
   * and read using {@link JsonReader#beginObject}.
   */
  BEGIN_OBJECT,

  /**
   * The closing of a JSON object. Written using {@link JsonWriter#endObject}
   * and read using {@link JsonReader#endObject}.
   */
  END_OBJECT,

  /**
   * A JSON property name. Within objects, tokens alternate between names and
   * their values. Written using {@link JsonWriter#name} and read using {@link
   * JsonReader#nextName}
   */
  NAME,

  /**
   * A JSON string.
   */
  STRING,

  /**
   * A JSON number represented in this API by a Java {@code double}, {@code
   * long}, or {@code int}.
   */
  NUMBER,

  /**
   * A JSON {@code true} or {@code false}.
   */
  BOOLEAN,

  /**
   * A JSON {@code null}.
   */
  NULL,

  /**
   * The end of the JSON stream. This sentinel value is returned by {@link
   * JsonReader#peek()} to signal that the JSON-encoded value has no more
   * tokens.
   */
  END_DOCUMENT
}

在调用ObjectTypeAdapter的read()方法时,所有数值类型NUMBER都转换成了Double类型,所以就有了前面出现的问题。到此,我们找到了问题的原因所在。出现这个问题,最根本的是Gson在使用ObjectTypeAdapter解析数值类型时,将其都当Double类型处理,而没有对类型进行细分处理。

解决方法

解决这个问题,大致有两种思路,一是修改NUMBER类型处理的源码,对其进行细化,也就是对ObjectTypeAdapter的read()方法中switch (token)语句进行细化。另一种是自定义一个适合于特定类型的类型适配器,可以参照ObjectTypeAdapter实现,根据前面定义的ResponseData类型,自己实现了一个ResponseData类型适配器ResponseDataTypeAdaptor,代码如下:

public class ResponseDataTypeAdaptor extends TypeAdapter<ResponseData> {

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

    private final Gson gson;

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

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

        out.beginObject();
        out.name("status");
        gson.getAdapter(Integer.class).write(out, value.getStatus());
        out.name("msg");
        gson.getAdapter(String.class).write(out, value.getMsg());
        out.name("data");
        gson.getAdapter(Object.class).write(out, value.getData());
        out.endObject();
    }

    @Override
    public ResponseData read(JsonReader in) throws IOException {
        ResponseData data = new ResponseData();
        Map<String, Object> dataMap = (Map<String, Object>) readInternal(in);
        data.setStatus((Integer) dataMap.get("status"));
        data.setMsg((String) dataMap.get("msg"));
        data.setData(dataMap.get("data"));
        return data;
    }


    private Object readInternal(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(readInternal(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(), readInternal(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();
        }
    }
}

涉及到NUMBER类型处理改动比较简单,如果待处理的原始数据中包含小数点或者是科学表示法则认为是浮点型,否则转化为整型。

实例验证

使用自定义的ResponseDataTypeAdaptor类型适配器,重新解析实例中的json字符串,测试代码如下:

public class GsonTest {

    @Test
    public void test() {
        String json = "{\"status\":200,\"msg\":\"OK\",\"data\":[{\"id\":1,\"username\":\"eric\",\"password\":\"123456\",\"age\":29,\"sex\":0,\"permission\":0,\"isDel\":0}]}";
        Gson gson = buildGson();
        ResponseData data = gson.fromJson(json, ResponseData.class);
        System.out.println(data.getData());
    }

    private  Gson buildGson() {
        GsonBuilder gsonBuilder = new GsonBuilder();
        gsonBuilder.registerTypeAdapterFactory(ResponseDataTypeAdaptor.FACTORY);
        return gsonBuilder.create();
    }
}

运行结果如下:

{"status":200,"msg":"OK","data":[{"id":1,"username":"eric","password":"123456","age":29,"sex":0,"permission":0,"isDel":0}]}
[{id=1, username=eric, password=123456, age=29, sex=0, permission=0, isDel=0}]

===============================================
Default Suite
Total tests run: 1, Failures: 0, Skips: 0
===============================================

可以发现,结果正确,整型的依然是整型,浮点型依旧为浮点型,问题得到解决。至此,有关Gson格式转换Integer变为Double类型问题原因分析以及解决方案就介绍到这,供大家参考。

© 著作权归作者所有

共有 人打赏支持
明MikeWoo
粉丝 5
博文 21
码字总数 13309
作品 0
闵行
后端工程师
私信 提问
加载中

评论(3)

9
92年的java
http://www.jujingyun.com 北京网站建设
http://www.shjuntang.com 上海装潢
明MikeWoo
明MikeWoo

引用来自“freezingsky”的评论

不管是Gson,还是Jackson, 对于类型或者序列化(反序列化)的控制 ,最终都是通过注入一个自己的解析器完成.
是的,工作中个人Gson用的比较多。
f
freezingsky
不管是Gson,还是Jackson, 对于类型或者序列化(反序列化)的控制 ,最终都是通过注入一个自己的解析器完成.
Java序列化与JSON序列化大比拼

一、背景 有项目需要传输Map结构的数据,有人倾向用Java序列化来做,有人倾向用JSON的序列化来做。所以我们还是比比吧。 Java观点:Object2Object,使用时简单快速。 JSON观点:JSON格式与语...

NoahX
2013/03/10
0
20
fastjson 1.1.49 发布,进一步提升性能

fastjson 1.1.49 发布了,进一步提升性能,提升json_string -> JSONObject之间的转换性能。在android 5下比原生org.json性能快30%,在android 6下接近原生org.json的性能。back port 1.2.x版...

oschina
2016/04/18
2.6K
7
Elasticsearch Date类型,时间存储相关说明

从昨晚开始,到今天中午之前,一直在纠结时间存储问题,昨晚是纠结时间取出来的问题。 其实我的想法很简单,我就想 存储到 Elasticsearch ,然后从 Elasticsearch 中再取出来的时候,它是个 ...

8446666
2016/07/04
2.3K
0
2.2 Java数据类型转换

数据类型的转换,分为自动转换和强制转换。自动转换是程序在执行过程中“悄然”进行的转换,不需要用户提前声明,一般是从位数低的类型向位数高的类型转换;强制类型转换则必须在代码中声明,...

李序锴
2017/12/28
0
0
Java基础之自动装箱和自动拆箱源码分析

自动装箱(boxing)和自动拆箱(unboxing) 首先了解下Java的四类八种基本数据类型 自动装箱 Java中所谓的装箱通俗点就是:八种基本数据类型在某些条件下使用时,会自动变为对应的包装器类型...

白志华
2015/09/23
928
0

没有更多内容

加载失败,请刷新页面

加载更多

看过上百部片子的这个人教你视频标签算法解析

本文由云+社区发表 随着内容时代的来临,多媒体信息,特别是视频信息的分析和理解需求,如图像分类、图像打标签、视频处理等等,变得越发迫切。目前图像分类已经发展了多年,在一定条件下已经...

腾讯云加社区
15分钟前
0
0
2. 红黑树

定义:红黑树(Red-Black Tree,简称R-B Tree),它一种特殊的二叉查找树(Binary Search Tree)。 要理解红黑树,先要了解什么是二叉查找树。在上一章中,我们学习了什么是二叉树,以及二叉树...

火拳-艾斯
16分钟前
0
0
input的button类型,点击页面跳转

一、input type=button 不做任何操作 例如: <input type="button" class="btn btn-primary" style="width: 30%" value="返回" onclick="window.location.href='/users/list'"></input> onc......

Sunki
22分钟前
0
0
踩坑:js 小数运算出现精度问题

背景 在学习小程序商城源码时发现了这个问题,单价可能出现小数,小数之间运算结果会莫名其妙多出一大串数字,比如下面这样👇。 在此之前我是知道 js 中著名的 0.1 + 0.2 != 0.3 的问题的,...

dkvirus
27分钟前
0
0
zookeeper和HBASE总结

zookeeper快速上手 zookeeper的基本功能和应用场景 zookeeper的整体运行机制 zookeeper的数据存储机制 数据存储形式 zookeeper中对用户的数据采用kv形式存储 只是zk有点特别: key:是以路径...

瑞查德-Jack
50分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部