您现在的位置是:首页 >技术杂谈 >Jackson2JsonRedisSerializer使用及问题网站首页技术杂谈

Jackson2JsonRedisSerializer使用及问题

帅气的梧桐述 2024-06-14 17:17:43
简介Jackson2JsonRedisSerializer使用及问题

1、使用

public static RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        lettuceConnectionFactory.setShareNativeConnection(false);
        RedisTemplate<String, Object> rt = new RedisTemplate<>();
        // 设置连接工厂
        rt.setConnectionFactory(lettuceConnectionFactory);
        // 设置 key 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        rt.setKeySerializer(stringRedisSerializer);
        rt.setHashKeySerializer(stringRedisSerializer);
        // 创建 JSON 序列化工具
        Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        /*ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(mapper);*/
        // 设置 value 的序列化
        rt.setValueSerializer(jsonRedisSerializer);
        rt.setHashValueSerializer(jsonRedisSerializer);
        rt.afterPropertiesSet();
        return rt;
    }

2、场景模拟

(1)不设置ObjectMapper直接发起set、get一个JSON对象操作结果

SET代码:

    @Test
    void redisSetTest() throws Exception {
        String jsonStr = "{"packetId":5,"packetHash":"a828f89bfde8639d2caab1ae9b1d953f34b407af1597cce9343620b99f995334","witnessNum":100,"remainWitnessNum":97,"blockNumber":5,"blockHash":"35354e07c9fb895c1c02851ca1e55c44fda87c21d87c3d47c8ffc20c8206e2a9","blockTimestamp":1682385969}";
        redisService.set("test1", JSON.parseObject(jsonStr));
    }

执行后,redis保存结果:

get代码:

    @Test
    void redisGetTest() throws Exception {
        Object test1 = redisService.get("test1");
        JSONObject jsonObject = JSON.parseObject((String) test1);
        System.out.println(jsonObject);
    }

执行结果:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to java.lang.String

 set进去的是一个JSONOBJECT,然后get出来的时候变成了LinkedHashMap。看源码:

Jackson2JsonRedisSerializer--》deserialize--》this.objectMapper.readValue--》
result = ctxt.readRootValue(p, valueType, this._findRootDeserializer(ctxt, valueType), (Object)null);

this._findRootDeserializer(ctxt, valueType)这代码最终返回的是:

UntypedObjectDeserializer

然后反序列化就看这个类的

deserialize(JsonParser p, DeserializationContext ctxt)

方法,里边mapObject源码里边就找到很多LinkedHashMap,也就是最终反序列化对象为此的缘由。

if (key1 == null) {
            return new LinkedHashMap(2);
        } else {
            p.nextToken();
            Object value1 = this.deserialize(p, ctxt);
            String key2 = p.nextFieldName();
            if (key2 == null) {
                LinkedHashMap<String, Object> result = new LinkedHashMap(2);
                result.put(key1, value1);
                return result;
            } else {
                p.nextToken();
                Object value2 = this.deserialize(p, ctxt);
                String key = p.nextFieldName();
                LinkedHashMap result;
                if (key == null) {
                    result = new LinkedHashMap(4);
                    result.put(key1, value1);
                    return result.put(key2, value2) != null ? this._mapObjectWithDups(p, ctxt, result, key1, value1, value2, key) : result;

 

 (2)设置ObjectMapper发起set、get一个JSON对象操作结果

将“使用”中注释掉的ObjectMapper代码放开,然后执行set:

    @Test
    void redisSetTest() throws Exception {
        String jsonStr = "{"packetId":5,"packetHash":"a828f89bfde8639d2caab1ae9b1d953f34b407af1597cce9343620b99f995334","witnessNum":100,"remainWitnessNum":97,"blockNumber":5,"blockHash":"35354e07c9fb895c1c02851ca1e55c44fda87c21d87c3d47c8ffc20c8206e2a9","blockTimestamp":1682385969}";
        redisService.set("test", JSON.parseObject(jsonStr));
    }

执行后,redis保存结果:

 

执行get方法:

    @Test
    void redisGetTest() throws Exception {
        Object test1 = redisService.get("test");
        JSONObject jsonObject = JSON.parseObject((String) test1);
        System.out.println(jsonObject);
    }

 返回结果,报错:java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to java.lang.String

这次是因为存redis的时候将对象的类型也同时放在VALUE中了,所以返回的时候就按照@class进行对象封装,故此时我们可以直接强转成指定的class即可。修改get方法如下:

    @Test
    void redisGetTest() throws Exception {
        Object test1 = redisService.get("test");
        System.out.println( (JSONObject) test1);
    }

此处获取的key:test是有@class的设置,但前边我们未设置ObjectMapper时设置的key:test1里边没有@class,此时获取test1的值时就报错:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class'

意思就是VALUE里边缺少@class属性,让他反序列化的时候不知道怎么处理了。因篇幅关系就不翻源码了。对比之前的代码主要就是后边设置ObjectMapper的属性,而这个@class是否设置根这个“JsonTypeInfo.As.PROPERTY”相关。假如把这个去掉,会不会就能获取了呢,答案时否定的,GET返回信息如下:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.lang.Object

这意思就是说返回值的格式不对,期望是个数组个数,结果返回的是单个OBJECT,为什么期望是数组呢。简单点我们把上一步的“JsonTypeInfo.As.PROPERTY”去掉之后,再调一下set方法,设置一个新值。观察VALUE变化,如下:

 发现,现在虽然没有@class了但是放一个OBJECT结果存到redis是个数组,把类型放到数组的第一个元素了。没法,继续上源码,看ObjectMapper源码:

    public ObjectMapper activateDefaultTyping(PolymorphicTypeValidator ptv) {
        return this.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
    }

    public ObjectMapper activateDefaultTyping(PolymorphicTypeValidator ptv, ObjectMapper.DefaultTyping applicability) {
        return this.activateDefaultTyping(ptv, applicability, As.WRAPPER_ARRAY);
    }

    public ObjectMapper activateDefaultTyping(PolymorphicTypeValidator ptv, ObjectMapper.DefaultTyping applicability, As includeAs) {
        if (includeAs == As.EXTERNAL_PROPERTY) {
            throw new IllegalArgumentException("Cannot use includeAs of " + includeAs);
        } else {
            TypeResolverBuilder<?> typer = this._constructDefaultTypeResolverBuilder(applicability, ptv);
            typer = typer.init(Id.CLASS, (TypeIdResolver)null);
            typer = typer.inclusion(includeAs);
            return this.setDefaultTyping(typer);
        }
    }

前边说的去掉“JsonTypeInfo.As.PROPERTY”,然后调用的ObjectMapper构造方法就对应前两个,我们可以看到As.WRAPPER_ARRAY,这从字面意思就大致知道需要是个ARRAY,实际也确实如此,后边再介绍这个具体作用。对一个上边的测试结果也就是,虽然去掉“JsonTypeInfo.As.PROPERTY”可以解决获取的VALUE值没有@class的问题,但是又需要VALUE值是个数组格式。

JsonTypeInfo.As介绍

  • PROPERTY 将对象类型包含在对象成员属性里
  • WRAPPER_OBJECT 将对象类型作为键,序列化的对象作为值
  • WRAPPER_ARRAY 第一个元素是对象类型,第二元素是序列化的对象

使用不同类别的VALUE呈现效果分别对应如下 :

  // Type name as a property, same as above
  {
    "@type" : "Employee",
     ...
  }
 
  // Wrapping class name as pseudo-property (similar to JAXB)
  {
    "com.fasterxml.beans.EmployeeImpl" : {
       ... // actual instance data without any metadata properties
    }
  }
 
  // Wrapping class name as first element of wrapper array:
  [
    "com.fasterxml.beans.EmployeeImpl",
    {
       ... // actual instance data without any metadata properties
    }
  ]

此时大概应该明白,为什么去掉“JsonTypeInfo.As.PROPERTY”仍然解析不出来没带对象类型的VALUE原因了。(看到这的人我很佩服你的坚持)。既然mapper.activateDefaultTyping不管怎么设置都需要对象类型,那么此时如果我们还是想要获取没有对象信息的值,怎么办呢(很多人可能对此已经没任何想法了,获取不到那就统一用有对象信息的方法放呗,折腾这鬼玩意。^_^,其实在跨语言用redis互通数据时,你就发现还是得折腾一下,O(∩_∩)O哈哈~,比如GO语言他负责SET数据,但是GO肯定是不知道JAVA的对象类型的,所以他也只能给你一个没对象信息的OBJECT,此时JAVA端不做点什么,那就只能骂娘了啊)

跨语言VALUE解析方法

上接提到的加入GO不知道JAVA对象信息,SET也没放对象信息,此时JAVA端该怎么GET数据。最简单的办法就是去掉ObjectMapper配置,此时所有类统一用LinkedHashMap处理,JAVA端GET到的数据是一个对象时,就用LinkedHashMap去接收,然后转换成自己的类。

另一种方法就是JAVA端该怎么配ObjectMapper还是怎么配,因为有ObjectMapper在JAVA端使用起来方便啊,类型放VALUE出来直接强转就完事了。让GO端SET时候直接放字符串,JAVA端用字符串GET即可。

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。