解决RedisTemplate的json反序列泛型丢失问题
背景
在使用redisTemplate操作redis时我们针对对象的序列化通常将序列化成json存储到redis。一般如下配置
@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类
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(); }
}
- 增加自定义编解码器
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); }
}
- 调用方式
SecurityUserInfo securityUserInfo = RedisDeserializeHelper.call(SecurityUserInfo.class, () -> redisTemplateObject.opsForValue().get(key));
总结
- 如果性能要求不高推荐使用方案二,对性能要求高可以参考方案三
- 另外对于redisson的序列化也会遇到相同的问题,但是redisson可以再从redis获取值时指定编解码器。所以这个问题影响比较小。