Redis的两种JSON的序列化器 GenericToStringSerializer和Jackson2JsonRedisSerializer

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 一切从一个错误开始说起Could not read JSON: Can not deserialize instance of com.springboot.entities.User out of START_ARRAY token

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

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

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
5天前
|
XML 存储 JSON
c#XML、JSON的序列化和反序列化,看完你就懂了
c#XML、JSON的序列化和反序列化,看完你就懂了
9 0
|
5天前
|
JSON Java Linux
【探索Linux】P.30(序列化和反序列化 | JSON序列化库 [ C++ ] )
【探索Linux】P.30(序列化和反序列化 | JSON序列化库 [ C++ ] )
20 2
|
17天前
|
JSON 编译器 Go
Golang深入浅出之-结构体标签(Tags):JSON序列化与反射应用
【4月更文挑战第22天】Go语言结构体标签用于添加元信息,常用于JSON序列化和ORM框架。本文聚焦JSON序列化和反射应用,讨论了如何使用`json`标签处理敏感字段、实现`omitempty`、自定义字段名和嵌套结构体。同时,通过反射访问标签信息,但应注意反射可能带来的性能问题。正确使用结构体标签能提升代码质量和安全性。
16 0
|
2月前
|
存储 JSON JavaScript
Python中的JSON与Pickle模块:数据序列化和反序列化的利器
在Python编程中,数据的序列化和反序列化是经常遇到的操作。序列化是将数据结构或对象状态转换为可以存储或传输的格式的过程,而反序列化则是这个过程的逆操作,即将序列化的数据重新转换回原来的数据结构或对象状态。Python中的JSON和Pickle模块就是实现数据序列化和反序列化的强大工具。
|
2月前
|
存储 JSON NoSQL
[Redis]——RedisTemplate的两种序列化方式
[Redis]——RedisTemplate的两种序列化方式
|
2月前
|
JSON Java Maven
使用Jackson进行 JSON 序列化和反序列化
使用Jackson进行 JSON 序列化和反序列化
29 0
|
2月前
|
存储 JSON 安全
序列化模块pickle和json有什么区别
序列化模块pickle和json有什么区别
22 0
|
3月前
|
JSON 数据格式 C++
[序列化协议] --- JSON
[序列化协议] --- JSON
35 0
|
4月前
|
JSON Java fastjson
Java中的JSON序列化和反序列化
Java中的JSON序列化和反序列化
|
4月前
|
JSON 机器人 数据格式
阿里云RPA支持将序列化的JSON数据作为输入参数传递给机器人应用程序
【1月更文挑战第7天】【1月更文挑战第33篇】阿里云RPA支持将序列化的JSON数据作为输入参数传递给机器人应用程序
212 1