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

如果有人能看到这里,说明你跟我一样有一个求知的心,而且你是一个能沉下心来学习的人,也许这些东西一辈子用不到,但是我觉得这种态度是好的,希望能给大家带来帮助,如有错误望指出,谢谢大家

上一篇:微信小程序把玩(四十)animation API


下一篇:今天看到有人在问SpringBoot自动装配原理。