Gson格式轉換Integer變爲Double類型問題解決

問題描述

在先後端分離的開發模式下,先後端交互一般採用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方法解析,java

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

解析後,結果以下:json

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值到一個對象的轉換。ide

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類型字段的類型適配器。ui

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字段數據的解析。this

/**
 * 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類型問題緣由分析以及解決方案就介紹到這,供你們參考。

相關文章
相關標籤/搜索