天天看點

Redis的兩種JSON的序列化器 GenericToStringSerializer和Jackson2JsonRedisSerializer

今天springboot整合redis時出現了Could not read JSON: Can not deserialize instance of com.springboot.entities.User out of START_ARRAY token錯誤,研究了半天才解決,想和大家分享一下。

以下内容為枯燥的源碼解讀,用GenericToStringSerializer替換Jackson2JsonRedisSerializer就可以解決問題,想看的小夥伴可以繼續,GenericToStringSerializer可能也存在localdata反序列化的問題,但是相容性相對較好

查了很多文章根本沒有糾這個錯的,跟蹤源碼發現是json反序列化時的報錯,看了一篇文章上說Jackson2JsonRedisSerializer反序列化會有問題推薦使用GenericToStringSerializer,奔着知其然知其是以然的态度,我翻看了GenericToStringSerializer的源碼,之是以GenericToStringSerializer反序列化更相容是因為兩個序列化器在序列化的時候産生的json串就不一樣,下面是兩個序列化器序列化list的結果

代碼一

Jackson2JsonRedisSerializer的


[
  {
    "id": 2,
    "userName": "1804350148",
    "password": "1228102568",
    "phone": "",
    "position": null,
    "userSex": -978393083
  },
  {
    "id": 3,
    "userName": "-49709306",
    "password": "-1978139369",
    "phone": "",
    "position": null,
    "userSex": 299438220
  }
] 

GenericToStringSerializer的

[
  "java.util.ArrayList",
  [
    {
      "@class": "com.springboot.entities.User",
      "id":   2,
      "userName": "1804350148",
      "password": "1228102568",
      "phone": "",
      "position":   null,
      "userSex":   -978393083
    },
    {
      "@class": "com.springboot.entities.User",
      "id":   3,
      "userName": "-49709306",
      "password": "-1978139369",
      "phone": "",
      "position":   null,
      "userSex":   299438220
    }
  ]
]           

很明顯結果不一樣,首先的先聊一聊我翻看源碼的心得吧

為什麼GenericToStringSerializer相容性好,是因為他在序列化對象的時候會在執行個體資料前加上對象類型,例如上例中的"java.util.ArrayList" ,那GenericToStringSerializer序列化器怎麼讓這個東西生效的呢,看源碼前我們先了解一下UTF8StreamJsonParser這個類,這個類幫我們封裝了反序列化的所有東西,包括從redis讀出來的byte數組啊,我們需要序列化成什麼類型啊,都是通過這個類幫我們完成的,我們先看一段源碼

代碼二

位于ObjectMapper.class中

  protected Object _readMapAndClose(JsonParser p0, JavaType valueType) throws IOException {
        JsonParser p = p0;
        Throwable var4 = null;
        Object var20;
        try {
6            JsonToken t = this._initForReading(p);
            Object result;
            if (t == JsonToken.VALUE_NULL) {
                DeserializationContext ctxt = this.createDeserializationContext(p, this.getDeserializationConfig());
                result = this._findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
            } else if (t != JsonToken.END_ARRAY && t != JsonToken.END_OBJECT) {
                DeserializationConfig cfg = this.getDeserializationConfig();
                DeserializationContext ctxt = this.createDeserializationContext(p, cfg);
                JsonDeserializer<Object> deser = this._findRootDeserializer(ctxt, valueType);
                if (cfg.useRootWrapping()) {
                    result = this._unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
                } else {
18                   result = deser.deserialize(p, ctxt);
               }
                ctxt.checkUnresolvedObjectId();
            } else {
                result = null;
            }           

代碼三

位于UTF8StreamJsonParser.class

1    private final int _skipWSOrEnd() throws IOException {
2        if (this._inputPtr >= this._inputEnd && !this._loadMore()) {
3           return this._eofAsNextChar();
4      } else {
5          int i = this._inputBuffer[this._inputPtr++] & 255;
           

這個方法太長了,就不全部拷貝了,我們重點關注代碼一的第六行,這個方法會調用代碼二的方法,然後我們看代碼二,我們重點關注第五行 this._inputBuffer就是我們從redis取出來的byte數組this._inputPtr這個變量的初始值為0記住他後面還要用,看上面的json串我們可以得到取出來的字元為‘[’,然後下面就會判斷一大堆然後給我們的UTF8StreamJsonParser設定一個叫currentToken的變量為 START_ARRAY, 這個代表着我們需要反序列化的類型為list,因為我們的所有list集合轉json第一個字元永遠是‘[’,感興趣的可以自己測試一下,細心地大神就會發現我們的錯誤資訊也有這個東西。設定這個東西有什麼用呢,我們繼續看上面源碼,目光投向第一段代碼的第18行,代碼中有标注,他會調用一個叫deserialize()的方法,這個方法是幹什麼用的呢,我們繼續看下面源碼

代碼四

位于UntypedObjectDeserializer.class中

public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
     switch(p.getCurrentTokenId()) {
            case 1:
            case 3:
            case 5:
                return typeDeserializer.deserializeTypedFromAny(p, ctxt);
            case 2:
            case 4:
            default:
                return ctxt.handleUnexpectedToken(Object.class, p);
            }
    }           

剛才的result = deser.deserialize(p, ctxt);這行代碼就會調用上述方法,方法沒複制全,比較長,怕影響大家積極性。感興趣的可以自己看一下,這個方法用了一個switch,用p.getCurrentTokenId()這個作為條件,這個東西是什麼呢,上面我們說他根據第一個字元'['判斷出我們需要的資料為list,給UTF8StreamJsonParser設定了一個currentToken,這個東西的id就是我們需要要的,START_ARRAY的id為3,但是3沒有任何處理,繼續往後找,找到了5,這個方法又有什麼用呢,我們繼續上源碼:

代碼六

public Object deserializeTypedFromAny(JsonParser p, DeserializationContext ctxt) throws IOException {
        return p.getCurrentToken() == JsonToken.START_ARRAY ? super.deserializeTypedFromArray(p, ctxt) : 
        this.deserializeTypedFromObject(p, ctxt);
}           

這個方法還是用到了我們原來設定的currentToken這個東西,進行一個判斷,我們調用super.deserializeTypedFromArray(p, ctxt)這個方法,然後還是繼續上源碼看看這個方法做了什麼

代碼七

protected Object _deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (((JsonParser)p).canReadTypeId()) {
            Object typeId = ((JsonParser)p).getTypeId();
            if (typeId != null) {
                return this._deserializeWithNativeTypeId((JsonParser)p, ctxt, typeId);
            }
        }

        boolean hadStartArray = ((JsonParser)p).isExpectedStartArrayToken();
9        String typeId = this._locateTypeId((JsonParser)p, ctxt);
        JsonDeserializer<Object> deser = this._findDeserializer(ctxt, typeId);
        if (this._typeIdVisible && !this._usesExternalId() && ((JsonParser)p).getCurrentToken() == JsonToken.START_OBJECT) {
            TokenBuffer tb = new TokenBuffer((ObjectCodec)null, false);
            tb.writeStartObject();
            tb.writeFieldName(this._typePropertyName);
            tb.writeString(typeId);
            ((JsonParser)p).clearCurrentToken();
            p = JsonParserSequence.createFlattened(false, tb.asParser((JsonParser)p), (JsonParser)p);
            ((JsonParser)p).nextToken();
        }

        Object value = deser.deserialize((JsonParser)p, ctxt);
        if (hadStartArray && ((JsonParser)p).nextToken() != JsonToken.END_ARRAY) {
            ctxt.reportWrongTokenException((JsonParser)p, JsonToken.END_ARRAY, "expected closing END_ARRAY after type information and deserialized value", new Object[0]);
        }

        return value;
    }
           

這次是核心代碼,我就全複制過來了

我們先關注第九行代碼吧,這個是确認反序列化類型的代碼我們進去看怎麼實作的

代碼八

protected String _locateTypeId(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (!p.isExpectedStartArrayToken()) {
            if (this._defaultImpl != null) {
                return this._idResolver.idFromBaseType();
            } else {
                ctxt.reportWrongTokenException(p, JsonToken.START_ARRAY, "need JSON Array to contain As.WRAPPER_ARRAY type information for class " + this.baseTypeName(), new Object[0]);
                return null;
            }
        } else {
11            JsonToken t = p.nextToken();
            if (t == JsonToken.VALUE_STRING) {
13            String result = p.getText();
                p.nextToken();
                return result;
            } else if (this._defaultImpl != null) {
                return this._idResolver.idFromBaseType();
            } else {
                ctxt.reportWrongTokenException(p, JsonToken.VALUE_STRING, "need JSON String that contains type id (for subtype of " + this.baseTypeName() + ")", new Object[0]);
                return null;
            }
        }
    }           

我們來關注11行代碼,這行代碼就是在為後面13行代碼擷取類型做鋪墊,其實這個方法前面已經使用過了,前面我們用這個方法找到主類型,這個我們需要用它找具體是哪種list,還是再寫一遍源碼吧,可能看上面不友善

代碼九

位于UTF8StreamJsonParser.class

1    private final int _skipWSOrEnd() throws IOException {
2        if (this._inputPtr >= this._inputEnd && !this._loadMore()) {
3           return this._eofAsNextChar();
4      } else {
5          int i = this._inputBuffer[this._inputPtr++] & 255;
           

我們仍然關注第5行,有人還記得我們上次用的時候++了嗎,是以這次我們取的是第二個字元'j',

代碼十

if (i == 34) {
      this._tokenIncomplete = true;
      this._nextToken = JsonToken.VALUE_STRING;
      return this._currToken;
  }
           

這個會設定我們的nextToken這個有什麼用呢,我們繼續讀代碼八13行,繼續上13行調用方法的源碼

代碼十一

protected String _finishAndReturnString() throws IOException {
        int ptr = this._inputPtr;
        if (ptr >= this._inputEnd) {
            this._loadMoreGuaranteed();
            ptr = this._inputPtr;
        }

        int outPtr = 0;
        char[] outBuf = this._textBuffer.emptyAndGetCurrentSegment();
        int[] codes = _icUTF8;
        int max = Math.min(this._inputEnd, ptr + outBuf.length);

        int c;
12       for(byte[] inputBuffer = this._inputBuffer; ptr < max; outBuf[outPtr++] = (char)c) {
            c = inputBuffer[ptr] & 255;
            if (codes[c] != 0) {
                if (c == 34) {
                    this._inputPtr = ptr + 1;
                    return this._textBuffer.setCurrentAndReturn(outPtr);
                }
                break;
20         }

            ++ptr;
        }

        this._inputPtr = ptr;
        this._finishString2(outBuf, outPtr);
        return this._textBuffer.contentsAsString();
    }           

這個方法 我們重點關注12到20行代碼,他通過一個for循環最後擷取一個字元串就是我們想要的java.util.ArrayList擷取後我們傳回,後面可能還通過相同的方式擷取元素的類型,最後反序列化,太不容易了,至此我們所有東西都有了,反序列化就擷取字元通過set綁定參數罷了

如果有人能看到這裡,說明你跟我一樣有一個求知的心,而且你是一個能沉下心來學習的人,也許這些東西一輩子用不到,但是我覺得這種态度是好的,希望能給大家帶來幫助,如有錯誤望指出,謝謝大家