Gson將json字串轉map導致int型被轉換成double的採坑之旅

Dog_brother發表於2019-05-02

前言:日常開發中,與json打交道的機會很多,一般物件json轉都不會出現什麼問題,但是json轉物件就有可能出現問題了,今天就來說說json轉map導致int型轉換成double的問題

問題重現

  • 之前解決過long型被轉化成科學計數法的問題,所有就拿以前的公用方法,一個泛型工具類
public class MyType<T> {
    public T gsonToMap(String strJson) {
        return new Gson().fromJson(strJson, new TypeToken<T>() {
        }.getType());
    }
}

String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
複製程式碼
  • 直接將需求型別物件傳入泛型就好了。
    Gson將json字串轉map導致int型被轉換成double的採坑之旅
  • 然而事與願違,int成功的轉換成double,1->1.0、0->0.0,如上圖所示

Gson將json字串轉map導致int型被轉換成double的採坑之旅

接下來的操作大家都知道了,藉助於網路平臺,於是乎找到幾種解決方式,細心的我發現有人評論解決他們的問題,看來有戲啊【手動滑稽】

解決方案

1、需要gson解析的型別 , 重寫他的deserialize方法, 就是將其中json手動解析成map , 不對資料進行處理

public HashMap<String,Object> gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(
                new TypeToken<HashMap<String,Object>>(){}.getType(),
                        new JsonDeserializer<HashMap<String, Object>>() {
                            @Override
                            public HashMap<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

                                HashMap<String, Object> hashMap = new HashMap<>();
                                JsonObject jsonObject = json.getAsJsonObject();
                                Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
                                for (Map.Entry<String, JsonElement> entry : entrySet) {
                                    hashMap.put(entry.getKey(), entry.getValue());
                                }
                                return hashMap;
                            }
                        }).create();

        return gson.fromJson(strJson, new TypeToken<HashMap<String,Object>>() {
        }.getType());
    }
複製程式碼
  • 經過實踐,是可以轉化成功,但是本著複用的思想,我把map替換成泛型,然後就不行,一臉矇蔽;(問題暫時擱置一旁)

2、自定義TypeAdapter替代Gson預設的adapter(此處埋下伏筆【偷笑】)解決,自定義TypeAdapter如下:

public class MapTypeAdapter extends TypeAdapter<Object> {

    private final TypeAdapter<Object> delegate = new Gson().getAdapter(Object.class);

    @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:
                /**
                 * 改寫數字的處理邏輯,將數字值分為整型與浮點型。
                 */
                double dbNum = in.nextDouble();

                // 數字超過long的最大值,返回浮點型別
                if (dbNum > Long.MAX_VALUE) {
                    return String.valueOf(dbNum);
                }

                // 判斷數字是否為整數值
                long lngNum = (long) dbNum;
                if (dbNum == lngNum) {
                    return String.valueOf(lngNum);
                } else {
                    return String.valueOf(dbNum);
                }

            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 {
        delegate.write(out,value);
    }
}
複製程式碼
  • 然後如法炮製,仍然固執的使用泛型,並將我們自定義的註冊到gson上
public T gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(new TypeToken<T>(){}.getType(),new MapTypeAdapter()).create();
        return gson.fromJson(strJson, new TypeToken<T>() {
        }.getType());
    }
    
String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
複製程式碼
  • 等待結果中...,每錯就是這麼刺激,int一樣會轉化成double

Gson將json字串轉map導致int型被轉換成double的採坑之旅

  • 把泛型直接替換成目標物件型別,再試了試,證明是沒問題的
public static Map<String, Object> gsonToMap(String strJson) {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(new TypeToken<Map<String,Object>>(){}.getType(),new MapTypeAdapter()).create();
        return gson.fromJson(strJson, new TypeToken<Map<String, Object>>() {
        }.getType());
    }
    
String json = "{\"identifier\":\"18111111111\",\"opType\":1,\"platform\":0}";
Map<String, Object> map = new MyType<Map<String, Object>>().gsonToMap(json);
複製程式碼

Gson將json字串轉map導致int型被轉換成double的採坑之旅

上述方案的確是可以解決我的問題,但是卻給我留下了疑問;本著知其然知其所以然的目的,覺得解決這些疑惑

解決疑惑

  • 為什麼傳遞泛型不行?
  • 為什麼是把int轉化成了double,而不是其他型別比如string?

1、關於泛型這裡就要提到 泛型擦除,及泛型只在編譯階段有效,執行時就無效了

  • 跟蹤原始碼會發現 TypeAdapter 就已經是一個泛型抽象類了
public abstract class TypeAdapter<T>
複製程式碼
  • 我在外層又傳了一次泛型,執行時根本就不認識我傳遞的目標物件型別了
    Gson將json字串轉map導致int型被轉換成double的採坑之旅
  • 在外層直接傳遞目標物件型別,這裡我傳遞的是HashMap<String,Object>,可我完全正確的識別出來

Gson將json字串轉map導致int型被轉換成double的採坑之旅

  • 所以我這裡的操作完全是符合泛型擦除,所以執行時程式碼根本不認識這是個什麼東西,自然不回你達到我們想要的效果了

2、int轉double,其實這是Gson在原始碼中故意為之的,其實不僅是int,long也會轉化成double,接下來我們去尋找證據

  • 跟蹤原始碼,走你 => 過程省略1000步,忽略1000000字,我們會來到Gson下的這個地方
    Gson將json字串轉map導致int型被轉換成double的採坑之旅
  • 這裡處理Number型的adapter,除此之外還有
處理double:
private TypeAdapter<Number> doubleAdapter(boolean serializeSpecialFloatingPointValues) {
    if (serializeSpecialFloatingPointValues) {
      return TypeAdapters.DOUBLE;
    }
    return new TypeAdapter<Number>() {
      @Override public Double read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }
        return in.nextDouble();
      }
      @Override public void write(JsonWriter out, Number value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }
        double doubleValue = value.doubleValue();
        checkValidFloatingPoint(doubleValue);
        out.value(value);
      }
    };
  }
  
  處理float:
  private TypeAdapter<Number> floatAdapter(boolean serializeSpecialFloatingPointValues) {
    if (serializeSpecialFloatingPointValues) {
      return TypeAdapters.FLOAT;
    }
    return new TypeAdapter<Number>() {
      @Override public Float read(JsonReader in) throws IOException {
        if (in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        }
        return (float) in.nextDouble();
      }
      @Override public void write(JsonWriter out, Number value) throws IOException {
        if (value == null) {
          out.nullValue();
          return;
        }
        float floatValue = value.floatValue();
        checkValidFloatingPoint(floatValue);
        out.value(value);
      }
    };
  }
複製程式碼
  • 其實這裡就是在尋找與我們目標物件想匹配的型別,但是如果找不到相匹配的型別,就會去呼叫 ObjectTypeAdapter,繼續跟蹤,它終於要在這裡正式尋找喜歡的介面卡了【斜眼笑】

Gson將json字串轉map導致int型被轉換成double的採坑之旅

  • 咋們運氣比較好,這 for (TypeAdapterFactory factory : factories) 裡有40幾個介面卡,第二個就是我們尋找的 ObjectTypeAdapter
    Gson將json字串轉map導致int型被轉換成double的採坑之旅
  • 它一看大家都是 T 就你跟我長得最像了,那就呼叫你了,於是乎就來到新世界
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);
  }
}
複製程式碼
  • 是不是跟我們之前自定義的adapter一模一樣,這就是為什麼我們要複寫這個TypeAdapter,重點看下面
case NUMBER:
      return in.nextDouble();
複製程式碼
  • 只要是Number(包括int、long、float、double等)型,都會被強制轉化成double,至於為什麼這麼做,因為這裡所有的型別都可以轉換成double,而反過來則不行。

其實,還有一個更簡單的方法,那就是不用Gson,比如使用FastJson,完全不會出現這個問題

Gson將json字串轉map導致int型被轉換成double的採坑之旅

後記:解決問題不是根本,需要尋找問題產生的根本,從根源上杜絕它的發生,這是以後需要多多加強的地方。加油(如有紕漏,歡迎指教)!!!

相關文章