解决RedisTemplate的json反序列泛型丢失问题

背景

在使用redisTemplate操作redis时我们针对对象的序列化通常将序列化成json存储到redis。一般如下配置

java 复制代码
@Bean  
@ConditionalOnMissingBean  
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory redisConnectionFactory,  
                                         ObjectProvider<RedisTemplateCustomizer> customizers) {  
    RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();  
    redisTemplate.setConnectionFactory(redisConnectionFactory);  
  
    StringRedisSerializer keySerializer = new StringRedisSerializer();  
    redisTemplate.setKeySerializer(keySerializer);  
    redisTemplate.setHashKeySerializer(keySerializer);  
  
    ObjectMapper objectMapper = ObjectMapperWrapper.getObjectMapper();  
    GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);  
    redisTemplate.setValueSerializer(valueSerializer);  
    redisTemplate.setHashValueSerializer(valueSerializer);  
  
    customizers.orderedStream().forEach((customizer) -> customizer.customize(redisTemplate));  
    return redisTemplate;  
}

使用GenericJackson2JsonRedisSerializer进行配置。但是这种方式会引发一个问题当进行反序列时如果是对象则会报错例如:
SecurityUserInfo o = (SecurityUserInfo) redisTemplateObject.opsForValue().get(key); 会报linkedHashMap无法转成具体的类型。因为序列化的json没有包含类型信息。只能按照默认的方式转换成linkedHashMap

解决方案

方案一

将jackson库的ObjectMapper序列化时带上类型信息mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); 但是这种方式会有几个缺点:

  • 增加redis存储,因为带上的类型信息
  • 可读性下降,类型信息会混淆在json中
  • 如果多个应用读写redis会增加理解成本
    所以此方案并不推荐

方案二

不使用activateDefaultTyping,查询的时候使用Object接收,然后使用mapper.convertValue方法转换。缺点是多一次序列化的操作,影响性能

方案三

曲线救国,使用ScopeValue将类型信息传递给RedisTemplate的序列化器(也可以用ThreadLocal),当反序列化时动态获取其类型。这个方式需要增加几个类,使用方式变化一下

  • 增加helper类
java 复制代码
import lombok.extern.slf4j.Slf4j;  
  
import java.util.concurrent.Callable;  
  
/**  
 * @author wxl  
 */@Slf4j  
@SuppressWarnings("all")  
public class RedisDeserializeHelper {  
  
    public static final ScopedValue<Class<?>> TYPE = ScopedValue.newInstance();  
  
    public static <R> R call(Class<R> clazz, Callable<Object> op) {  
        try {  
            Object call = ScopedValue.where(TYPE, clazz).call(op);  
            if (call == null) {  
                return null;  
            }  
            if (clazz.isAssignableFrom(call.getClass())) {  
                return (R) call;  
            }  
            return (R) call;  
        } catch (Exception e) {  
            log.error("redis deserialize failed", e);  
            throw new RuntimeException(e);  
        }  
    }  
  
    public static Class<?> get() {  
        return TYPE.get();  
    }  
}
  • 增加自定义编解码器
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;  
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;  
import org.springframework.data.redis.serializer.SerializationException;  
  
/**  
 * @author wxl  
 */public class SofastGenericJackson2JsonRedisSerializer extends GenericJackson2JsonRedisSerializer {  
  
    public SofastGenericJackson2JsonRedisSerializer(ObjectMapper objectMapper) {  
        super(objectMapper);  
    }  
  
    @Override  
    public Object deserialize(byte[] source) throws SerializationException {  
        Class<?> clazz = RedisDeserializeHelper.get();  
        if (clazz != null) {  
            return deserialize(source, clazz);  
        }  
        return super.deserialize(source);  
    }  
  
    @Override  
    public <T> T deserialize(byte[] source, Class<T> type) throws SerializationException {  
        return super.deserialize(source, type);  
    }  
}
  • 调用方式
java 复制代码
SecurityUserInfo securityUserInfo = RedisDeserializeHelper.call(SecurityUserInfo.class, () -> redisTemplateObject.opsForValue().get(key));

总结

  • 如果性能要求不高推荐使用方案二,对性能要求高可以参考方案三
  • 另外对于redisson的序列化也会遇到相同的问题,但是redisson可以再从redis获取值时指定编解码器。所以这个问题影响比较小。
相关推荐
小羊没烦恼!1 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里1 天前
java中的继承和多态的区别
java
小羊没烦恼!1 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕1 天前
JSONObject与JSONArray封装数据格式区别
java·json
A黄俊辉A1 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
胡写代码1 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖1 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时1 天前
01-06-A-JVM排查实战详解
java·jvm
伞伞悦读1 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
vipxieliang1 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot