今天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绑定参数罢了