Android自帶Json庫使用引發的問題

niveka發表於2018-06-13

在Andriod系統應用層開發通常json協議解析使用Gson、jackson當然還公司的fastjson庫等,Andriod其實也自帶json解析庫,整合的是apache的,在一些特定的場景用自帶庫解析也很方便。
但是,不得不說自帶庫有個坑踩進去了就會被坑的挺慘,而且很難發現到問題;

一、背景

我們的專案部分模組在http請求時涉及到對引數key value計算出md5,通過json協議資料傳輸,到了服務端再做md5的校驗,正常來說計算md5的規則雙方都做了統一保證,滿足了一致性的條件。理論上,只要通訊過程資料未發生篡改,100%能保證是一致的;但是問題來了,即使中間的通訊資料資料未被篡改,雙方計算出來的md5還是存在不匹配的情形,而且出現的問題斷斷續續,一直沒有得到有效定位和解決。

18_08_39__06_07_2018.jpg

然而並沒有想的那樣100%md5計算相同

二、排查路徑

2.1 分析現象

問題出現時會一直提示md5校驗失敗,說明兩邊的md5計算結果確實不一樣,然而發生的概率很低,低到幾乎可以忽略不計,但只要出現問題就能穩定復現。


2.2 定位問題

首先,很容易想到的是雙方計算規則不同,計算的層級不同,畢竟Android端對庫的依賴和服務端庫的依賴存在這差別;然而,將演算法統一校準後問題並沒有得到解決~

再來從有問題的請求json串入手分析,發現帶問題的json資料給到服務端解析後–出現json轉化的值一些些特定字元都會被去掉,那問題其實就定位到了,但這個服務端的問題嗎?畢竟它每次都會將值裡邊的某個字元給丟掉。查下json規範,http://www.rfc-editor.org/rfc/rfc4627.txt(RFC 4627)轉義符號會被當作無效字元給丟棄,說的也很清楚。

18_32_19__06_07_2018.jpg
很明顯編譯器也過不了這種規則,但是json資料傳輸時這串是能成立的

那是資料獲取源頭產生的問題嗎,它是否在執行過程中就是產生了這種string值?動態除錯了一番發現在欄位賦值的時候的確是沒有轉義字元的(“)。很明顯了,就是在轉換成json的時候被加上轉義符了,這也很難和md5計算扯上聯絡對吧?關鍵的點來了,因為一直以來都是在最後的封裝環節把資料封裝好了資料才進行md5計算,這個思路和方案都沒有問題的(不可能提前知曉所有欄位和值吧?),那就說明是使用系統json庫取值的時候出了問題,讓轉義符也參與了計算,看原始碼部分。


//opt是JSONObject
if (opt.getClass().isPrimitive()) {
    return opt.toString();
 }

問題是定位到了,那這是很神奇的問題啊,讓我們從原始碼來看看~

  • Android系統自帶json庫
//org.json.JSONStringer
private void string(String value) {
        out.append(""");
        for (int i = 0, length = value.length(); i < length; i++) {
            char c = value.charAt(i);

            /*
             * From RFC 4627, "All Unicode characters may be placed within the
             * quotation marks except for the characters that must be escaped:
             * quotation mark, reverse solidus, and the control characters
             * (U+0000 through U+001F)."
             */
            switch (c) {
                case `"`:
                case `\`:
                case `/`:
                    out.append(`\`).append(c);//看這
                    break;

                case `	`:
                    out.append("\t");
                    break;

                case ``:
                    out.append("\b");
                    break;

                case `
`:
                    out.append("\n");
                    break;

                case `
`:
                    out.append("\r");
                    break;

                case `f`:
                    out.append("\f");
                    break;

                default:
                    if (c <= 0x1F) {
                        out.append(String.format("\u%04x", (int) c));
                    } else {
                        out.append(c);
                    }
                    break;
            }

        }
        out.append(""");
    }

其轉義時會將字元“插入需要轉義的前一位,下面對比Gson的解析和封裝。


  • Gson解析json
https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/stream/JsonReader.java

 /**
   * Returns the string up to but not including {@code quote}, unescaping any
   * character escape sequences encountered along the way. The opening quote
   * should have already been read. This consumes the closing quote, but does
   * not include it in the returned string.
   *
   * @param quote either ` or ".
   * @throws NumberFormatException if any unicode escape sequences are
   *     malformed.
   */
  private String nextQuotedValue(char quote) throws IOException {
    // Like nextNonWhitespace, this uses locals `p` and `l` to save inner-loop field access.
    char[] buffer = this.buffer;
    StringBuilder builder = null;
    while (true) {
      int p = pos;
      int l = limit;
      /* the index of the first character not yet appended to the builder. */
      int start = p;
      while (p < l) {
        int c = buffer[p++];

        if (c == quote) {
          pos = p;
          int len = p - start - 1;
          if (builder == null) {
            return new String(buffer, start, len);
          } else {
            builder.append(buffer, start, len);
            return builder.toString();
          }
        } else if (c == `\`) {//看這
          pos = p;
          int len = p - start - 1;
          if (builder == null) {
            int estimatedLength = (len + 1) * 2;
            builder = new StringBuilder(Math.max(estimatedLength, 16));
          }
          builder.append(buffer, start, len);
          builder.append(readEscapeCharacter());
          p = pos;
          l = limit;
          start = p;
        } else if (c == `
`) {
          lineNumber++;
          lineStart = p;
        }
      }

      if (builder == null) {
        int estimatedLength = (p - start) * 2;
        builder = new StringBuilder(Math.max(estimatedLength, 16));
      }
      builder.append(buffer, start, p - start);
      pos = p;
      if (!fillBuffer(1)) {
        throw syntaxError("Unterminated string");
      }
    }
  }

其寫方法

static {

    REPLACEMENT_CHARS = new String[128];

    for (int i = 0; i <= 0x1f; i++) {

      REPLACEMENT_CHARS[i] = String.format("\u%04x", (int) i);

    }

    REPLACEMENT_CHARS[`"`] = "\"";

    REPLACEMENT_CHARS[`\`] = "\\";

    REPLACEMENT_CHARS[`	`] = "\t";

    REPLACEMENT_CHARS[``] = "\b";

    REPLACEMENT_CHARS[`
`] = "\n";

    REPLACEMENT_CHARS[`
`] = "\r";

    REPLACEMENT_CHARS[`f`] = "\f";

    HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone();

    HTML_SAFE_REPLACEMENT_CHARS[`<`] = "\u003c";

    HTML_SAFE_REPLACEMENT_CHARS[`>`] = "\u003e";

    HTML_SAFE_REPLACEMENT_CHARS[`&`] = "\u0026";

    HTML_SAFE_REPLACEMENT_CHARS[`=`] = "\u003d";

    HTML_SAFE_REPLACEMENT_CHARS[```] = "\u0027";

  }

gson是不會對`/`進行轉義的,那是否直接使用gson庫替換就解決問題?應該還不是這麼簡單的,這裡我們注意到仍然是存在特殊字元需要轉義的,最後還是得回到調整取json字元值上面來。到這裡問題就定位的很清楚了。


2.3 評估影響面

這個其實影響很大的,一直以來就很難發現潛在的問題(概率低),尤其是作為通訊最基礎關鍵的部分。每次請求都可能會觸發到特殊字元的轉義,那後面再從json物件取到的字串string值都是帶著轉義符號過來的。


2.4 解決方案

兩個解決方案:
一是,對從json物件去除字串進行二次加工,呼叫去除轉義的方法把轉義符號給去除。
二是,將json物件解析回java物件,保證每次取到的都是正確的值。

方案一需要重寫去除轉義的方法,方案二會一定程度的影響到效能。那還是使用方案一來修復;


小結

此坑到此為止~~~!


相關文章