問題描述
在前后端分離的開發模式下,前后端交互通常采用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類型問題原因分析以及解決方案就介紹到這,供大家參考。
原文鏈接:Gson格式轉換Integer變為Double類型問題解決