Gson轉(zhuǎn)換錯(cuò)誤導(dǎo)致Int變?yōu)镈ouble類型

問題描述

埋點(diǎn)系統(tǒng)負(fù)責(zé)接收客戶端唇聘、H5等系統(tǒng)發(fā)送過來的用戶行為埋點(diǎn)數(shù)據(jù),經(jīng)過統(tǒng)一的接收讨韭、解析脂信,最終發(fā)到Kafka中,提供給下游業(yè)務(wù)方進(jìn)行消費(fèi)透硝。在一個(gè)變更測試中狰闪,發(fā)現(xiàn)原本是整型的數(shù)據(jù)轉(zhuǎn)換后變成了double。為了問題描述簡單濒生,便于大家理解埋泵,簡化為下面的例子,本文源碼基于gson-2.8.0罪治。

{
    "rate": 1.0,
    "extend": {
        "number": 30,
        "amount": 120.3
    }
}

處理后邊變?yōu)椋?/p>

{
"rate":1.0,
"extend":{
"number":30.0,
"amount":120.3
}
}

如extend字段中的number的值的從整型30變?yōu)榱薲ouble類型丽声,剛好下游業(yè)務(wù)方有些是把數(shù)值型轉(zhuǎn)換為字符串類型進(jìn)行邏輯判斷,比如兩張表的join操作的時(shí)候觉义,由于類型發(fā)生了切換雁社,導(dǎo)致關(guān)聯(lián)不上,字符串"30"和"30.0"不相等晒骇。

代碼分析

看到上面的部分霉撵,可能有些同學(xué)會(huì)說了,為什么不直接把number字段定義為整型來規(guī)避這個(gè)問題洪囤。此處的原因是extend字段是擴(kuò)展字段徒坡,不確定里面包含哪些字段,跟業(yè)務(wù)的上報(bào)方密切相關(guān)箍鼓。
Data類定義

public class Data {
    private Double rate;

    private Object extend;

    public Double getRate() {
        return rate;
    }

    public void setRate(Double rate) {
        this.rate = rate;
    }

    @Override
    public String toString() {
        return "Data{" +
                "rate=" + rate +
                ", extend=" + extend +
                '}';
    }
}

再看下測試代碼:

public class GsonTest {
    public static void main(String[] args) {
        String dataJson = "{\"rate\" : 1.0, \"extend\" : {\"number\" : 30, \"amount\" : 120.3}}";
        Gson gson = buildGson();
        Data data = gson.fromJson(dataJson, Data.class);
        System.out.println(data.toString());
    }


    private static Gson buildGson() {
        GsonBuilder gsonBuilder = new GsonBuilder();
        return gsonBuilder.create();
    }
}

輸出結(jié)果:


結(jié)果輸出

根因分析

接下來我們簡述下反序列化的過程崭参,Gson根據(jù)待解析的類型定位到具體的TypeAdaptor<T>類,其接口的主要方法如下:

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;
}

通過read方法從JsonReader中讀取相應(yīng)的數(shù)據(jù)組裝成最終的對(duì)象款咖,由于Data類中的extend字段的聲明類型是Object何暮,最終Gson會(huì)定位到內(nèi)置的ObjectTypeAdaptor類奄喂,我們來分析一下該類的邏輯過程。

/**
 * 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();
      //數(shù)值類型全部轉(zhuǎn)換為了Double類型
    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);
  }
}

看到該邏輯過程我們看到海洼,如果Json對(duì)應(yīng)的是Object類型跨新,最終會(huì)解析為Map<String, Object>類型;其中Object類型跟Json中具體的值有關(guān)坏逢,比如雙引號(hào)的""值翻譯為STRING域帐。我們可以看下數(shù)值類型(NUMBER)全部轉(zhuǎn)換為了Double類型,所以就有了我們之前的問題是整,整型數(shù)據(jù)被翻譯為了Double類型肖揣,比如30變?yōu)榱?0.0「∪耄看到這龙优,大家是不是也在想應(yīng)該細(xì)分下NUMBER數(shù)值類型,按照整型和浮點(diǎn)型分開處理事秀,我們看下JsonToken是否有更細(xì)分的類型彤断。

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
}

居然沒有細(xì)分類型,那這怎么辦易迹。?沒事宰衙,我們?cè)俜治鱿翵sonReader.peek方法

 /**
   * Returns the type of the next token without consuming it.
   */
  public JsonToken peek() throws IOException {
    int p = peeked;
    if (p == PEEKED_NONE) {
      p = doPeek();
    }

    switch (p) {
    case PEEKED_BEGIN_OBJECT:
      return JsonToken.BEGIN_OBJECT;
    case PEEKED_END_OBJECT:
      return JsonToken.END_OBJECT;
    case PEEKED_BEGIN_ARRAY:
      return JsonToken.BEGIN_ARRAY;
    case PEEKED_END_ARRAY:
      return JsonToken.END_ARRAY;
    case PEEKED_SINGLE_QUOTED_NAME:
    case PEEKED_DOUBLE_QUOTED_NAME:
    case PEEKED_UNQUOTED_NAME:
      return JsonToken.NAME;
    case PEEKED_TRUE:
    case PEEKED_FALSE:
      return JsonToken.BOOLEAN;
    case PEEKED_NULL:
      return JsonToken.NULL;
    case PEEKED_SINGLE_QUOTED:
    case PEEKED_DOUBLE_QUOTED:
    case PEEKED_UNQUOTED:
    case PEEKED_BUFFERED:
      return JsonToken.STRING;
    case PEEKED_LONG:
    case PEEKED_NUMBER:
      return JsonToken.NUMBER;
    case PEEKED_EOF:
      return JsonToken.END_DOCUMENT;
    default:
      throw new AssertionError();
    }
  }

可以看到其實(shí)在JsonReader的讀取過程中是有細(xì)分整型和浮點(diǎn)型,可以對(duì)外轉(zhuǎn)換后不再區(qū)分?jǐn)?shù)值類型了睹欲,一種改法是直接修改源碼供炼,在JsonToken多定義定義一個(gè)整型Long,然后在讀取的過程中細(xì)分下類型窘疮,修改ObjectTypeAdaptor的方法后大概如下所示

  @Override public Object read(JsonReader in) throws IOException {
    JsonToken token = in.peek();
    switch (token) {
        ..........................

      case LONG:
        return in.nextLong();

        case NUMBER:
            return in.nextDouble();

        ..........................
    }
}

什么劲蜻,居然要修改源碼,是不是改動(dòng)太大了?加唷!轧苫!我們?cè)倩氐街暗闹R(shí)點(diǎn)楚堤,解析方式是根據(jù)類型找到具體的TypeAdaptor,同時(shí)我們不希望改變JsonToken等類的實(shí)現(xiàn)含懊。所以我們首先為Data定義一個(gè)適配器身冬,命名為DataTypeAdaptor,具體實(shí)現(xiàn)如下:

public class DataTypeAdaptor extends TypeAdapter<Data> {

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

    private final Gson gson;

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

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

        out.beginObject();
        out.name("rate");
        gson.getAdapter(Double.class).write(out, value.getRate());
        out.name("extend");
        gson.getAdapter(Object.class).write(out, value.getExtend());
        out.endObject();
    }

    @Override
    public Data read(JsonReader in) throws IOException {
        Data data = new Data();
        Map<String, Object> dataMap = (Map<String, Object>) readInternal(in);
        data.setRate((Double) dataMap.get("rate"));
        data.setExtend(dataMap.get("extend"));
        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:
                //將其作為一個(gè)字符串讀取出來
                String numberStr = in.nextString();
                //返回的numberStr不會(huì)為null
                if (numberStr.contains(".") || numberStr.contains("e")
                     || numberStr.contains("E")) {
                    return Double.parseDouble(numberStr);
                }
                return Long.parseLong(numberStr);

            case BOOLEAN:
                return in.nextBoolean();

            case NULL:
                in.nextNull();
                return null;

            default:
                throw new IllegalStateException();
        }
    }
}

改動(dòng)點(diǎn)為讀取數(shù)值類型的時(shí)候按照字符串讀取岔乔,如果原始數(shù)據(jù)中包含小數(shù)點(diǎn)或者是科學(xué)表示法則認(rèn)為是浮點(diǎn)型酥筝,否則則是整型。再回過頭的看下原始的例子

public class GsonTest {
    public static void main(String[] args) {
        String dataJson = "{\"rate\" : 1.0, \"extend\" : {\"number\" : 30, \"amount\" : 120.3}}";
        Gson gson = buildGson();
        Data data = gson.fromJson(dataJson, Data.class);
        System.out.println(data.toString());
        System.out.println(gson.toJson(data, Data.class));
    }


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

運(yùn)行結(jié)果

Data{rate=1.0, extend={number=30, amount=120.3}}
{"rate":1.0,"extend":{"number":30,"amount":120.3}}

Process finished with exit code 0

結(jié)果正確雏门,整型的依然是整型嘿歌,浮點(diǎn)型依舊為浮點(diǎn)型掸掏,問題得到解決。對(duì)于問題本身其實(shí)應(yīng)該推動(dòng)業(yè)務(wù)方去按照schema類型進(jìn)行整改宙帝,由于本文主要討論gson丧凤,在此不再贅述其它解決方式。另外其實(shí)個(gè)人覺得Gson本身應(yīng)該區(qū)分開來整型和浮點(diǎn)型步脓,從代碼的情況來看愿待,其應(yīng)該是考慮了該問題,但是最終卻沒有開發(fā)給用戶靴患,暫不得其解仍侥,后續(xù)準(zhǔn)備在社區(qū)里咨詢?cè)搯栴}。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸳君,一起剝皮案震驚了整個(gè)濱河市农渊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌相嵌,老刑警劉巖腿时,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饭宾,居然都是意外死亡批糟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門看铆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徽鼎,“玉大人,你說我怎么就攤上這事弹惦》裼伲” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵棠隐,是天一觀的道長石抡。 經(jīng)常有香客問我,道長助泽,這世上最難降的妖魔是什么啰扛? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮嗡贺,結(jié)果婚禮上隐解,老公的妹妹穿的比我還像新娘。我一直安慰自己诫睬,他們只是感情好煞茫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般续徽。 火紅的嫁衣襯著肌膚如雪蚓曼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天炸宵,我揣著相機(jī)與錄音辟躏,去河邊找鬼。 笑死土全,一個(gè)胖子當(dāng)著我的面吹牛捎琐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播裹匙,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瑞凑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了概页?” 一聲冷哼從身側(cè)響起籽御,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惰匙,沒想到半個(gè)月后技掏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡项鬼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年哑梳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绘盟。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸠真,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出龄毡,到底是詐尸還是另有隱情吠卷,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布沦零,位于F島的核電站祭隔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏路操。R本人自食惡果不足惜序攘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寻拂。 院中可真熱鬧,春花似錦丈牢、人聲如沸祭钉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽慌核。三九已至距境,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垮卓,已是汗流浹背垫桂。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留粟按,地道東北人诬滩。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像灭将,于是被迫代替她去往敵國和親疼鸟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容