您现在的位置是:首页 >技术杂谈 >Jackson2JsonRedisSerializer使用及问题网站首页技术杂谈
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即可。