今天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綁定參數罷了