GenericJackson2JsonRedisSerializer问题 & Jackson2JsonRedisSerializer & StringRedisSerializer
【代码】GenericJackson2JsonRedisSerializer。
学习链接
Jackson序列化(5) — JacksonObjectMapper.DefaultTyping.NON_FINAL属性
Jackson 解决没有无参构造函数的反序列化问题
Java中没有无参构造方法的类反序列化解决方案
RedisTemplate配置的jackson.ObjectMapper里的一个enableDefaultTyping方法过期解决
详解jackson注解(一)jackson反系列化注解
详解jackson注解(二)jackson反系列化注解
springboot集成redis,使用jackson序列化方案报Type id handling not implemented for 错误问题处理
Redis序列化存储及其日期格式问题
其中,有两个问题:
- 一个是序列化时,jackson通过DefaultTyping把类型写进去,反序列化的时候,就不用传具体的类型了;
- 反序列化时,由于某些类是框架里面的,我们无法为其添加无参构造方法,并且它只有有参构造方法,需要在框架的基础上,自定义处理方式。
上面2个问题是比较棘手的,在上面的学习链接上面都有对应的解决方式。
GenericJackson2JsonRedisSerializer
-
对于这个不存在的属性, 需要标注为忽略, 否则, 反序列化时, 会报错。
-
主要也是因为,我们拿不到GenericJackson2JsonRedisSerializer中的mapper,不然,是可以修改mapper的反序列化特性的,把mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);关闭也可。
-
发现,在反序列化的时候,我们
并未提供具体反序列化的类!!!
,但是GenericJackson2JsonRedisSerializer却可以帮助我们反序列化为当时指定的类
,并且注意到,在序列化的时候,是存在@class这个字段的 -
通过
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY
);可以将类型写入json中,反序列化时,就不用提供具体的类。但是,这样改造会存在风险,一旦这个序列化的这个类换了位置,redis里面的对象将不能被反序列化回来,那redis里面这个存的的东西就废了,但是,我们修改成这种序列化的初衷就是让redis里存的数据能够以可视化的直观的能够看到,这个目的的确是达到了,但是,生成的json无法通用,即:别人无法解析出来。可是,即使采用jdk的序列化方式,也有这样的问题,这样看来,它还是可以的,但要注意到这个风险。 -
还有一个问题,上面我们知道了,ObjectMapper在序列化一个我们自定义对象,如果开启了DefaultTyping,会把对应的类型也插入到了json中,方便反序列化回来。但是,
如果写入一个字符串,这个ObjectMapper会写出个什么东西呢?它会在字符串的两边加上双引号,并且写入的字符串中间有双引号,它会自动在双引号的前面来个反斜杠\。
(如:写入hal"o
这5个字符,它写出来是:"hal\"o"
有8个字符,类似于我们在开发工具上的写法,但是那个\
只是转义,它这个反斜杠\
是实际存在的)。但是,如果写入一个对象,对象的某个属性是字符串类型,这个属性里面有单个双引号,那么这个对象在写出json的时候,里面的这个属性的单个双引号前面会加上反斜杠,这样也可以反序列化过来,因为,如果json里面没有这个反斜杠的话,就无法解析了,这个可以理解。- 看到了ObjectMapper这样处理我们的字符串,我们其实并不希望它这么做,那我如果把一个json字符串交给它,它就在json字符串里面的所有双引号前面,都来这么一出,整个json字符串就显得很乱,要是这个json字符串有很多级,那到处都是反斜杠,没法看。
这就是为什么需要StringRedisSerializer了,遇到字符串,直接获取字符串的字节数据,把字节数据写到redis里面就可以了
。
package com.zzhua.blog.config.redis; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.Data; import org.junit.Test; import java.util.Date; @Data public class Person { private String name; private Integer age; private Date date; @JsonIgnore /* 对于这个不存在的属性, 需要标注为忽略, 否则, 反序列化时, 会报错。 */ public Integer getEven() { return this.age % 2 == 0?1:0; } public Person() { } public Person(String name, Integer age,Date date) { this.name = name; this.age = age; this.date = date; } }
@Test public void test001() { GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); byte[] bytes = jackson2JsonRedisSerializer.serialize(new Person("zzhua", 19, new Date())); String s = new String(bytes, StandardCharsets.UTF_8); System.out.println(s); /* {"@class":"com.zzhua.blog.config.redis.Person","name":"zzhua","age":19,"date":["java.util.Date",1682245115549]} */ Object o = jackson2JsonRedisSerializer.deserialize(s.getBytes(StandardCharsets.UTF_8)); System.out.println(o); // Person(name=zzhua, age=19, date=Sun Apr 23 18:18:35 CST 2023) } /* 和下面对比一下,原始的ObjectMapper的用法 */ @Test public void test002() throws Exception { ObjectMapper mapper = new ObjectMapper(); System.out.println(mapper.writeValueAsString(new Person("zzhua",19, new Date()))); /* {"name":"zzhua","age":19,"date":1682243545590} */ mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); Person person = mapper.readValue("{\"name\":\"zzhua\",\"age\":19,\"date\":1682243545590,\"even\":0}", Person.class); System.out.println(person); } // ObjectMapper中 开启了DefaultTyping, 会把类名也写进去, 反序列化时, 只需要指定Object.class也能得到序列化之前的对象 @Test public void test003() throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); System.out.println(mapper.writeValueAsString(new Person("zzhua",19, new Date()))); // {"@class":"com.zzhua.blog.config.redis.Person","name":"zzhua","age":19,"date":["java.util.Date",1682512467607]} Object o = mapper.readValue( "{\"@class\":\"com.zzhua.blog.config.redis.Person\",\"name\":\"zzhua\",\"age\":19,\"date\":[\"java.util.Date\",1682512467607]}", Object.class); System.out.println(o); // Person(name=zzhua, age=19, date=Wed Apr 26 20:34:27 CST 2023) }
- 看到了ObjectMapper这样处理我们的字符串,我们其实并不希望它这么做,那我如果把一个json字符串交给它,它就在json字符串里面的所有双引号前面,都来这么一出,整个json字符串就显得很乱,要是这个json字符串有很多级,那到处都是反斜杠,没法看。
-
Result是常用的返回类,带泛型,下面仍然能够保留原类信息
@Test public void test003() { GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); Result<Person> result = new Result<>(); result.setData(new Person("zzhua", 19, new Date())); byte[] bytes = jackson2JsonRedisSerializer.serialize(result); String s = new String(bytes, StandardCharsets.UTF_8); System.out.println(s); /* {"@class":"com.zzhua.blog.util.Result","code":0,"msg":null,"data":{"@class":"com.zzhua.blog.config.redis.Person","name":"zzhua","age":19,"date":["java.util.Date",1682245790213]}}*/ Object o = jackson2JsonRedisSerializer.deserialize(s.getBytes(StandardCharsets.UTF_8)); System.out.println(o); /* Result(code=0, msg=null, data=Person(name=zzhua, age=19, date=Sun Apr 23 18:29:50 CST 2023)) */ }
Jackson2JsonRedisSerializer
模拟Jackson2JsonRedisSerializer内部使用ObjectMapper,并且使用Object.class解析的javaType来做测试
public class TestObjectMapper {
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("zzhua");
ObjectMapper mapper = new ObjectMapper();
byte[] bytes = mapper.writeValueAsBytes(person);
JavaType javaType = TypeFactory.defaultInstance().constructType(Object.class);
Object o = mapper.readValue(bytes, javaType);
System.out.println(o);
}
}
/*
输出:class java.util.LinkedHashMap
{name=zzhua}
*/
StringRedisSerializer
为什么会存在这个redis序列化器呢?上面我们看到了Jackson2JsonRedisSerializer的序列化,它实际上就是依赖于ObjectMapper的功能实现的,但是它这种序列化会存在风险,就是开启了DefaultTyping之后,序列化后的json无法做到通用。
那我们可以这么做,先把一个对象写成json字符串,然后把这个字符串写到redis里面
。源码里面的实现也很简单。可以看到,里面并没有使用Jackson的ObjectMapper。
其实,还要注意一点哦,我们如果如果打算按上面说的这么做,配置了RedisTemplate<String,Object>的值序列化方式为Jackson2JsonRedisSerializer,那么存入json的时候,千万别注入这个redisTemplate,而要注入StringRedisTemplate
。因为,如果使用redisTemplate的话,写出去的值是对象的json字符串,我们已经知道了ObjectMapper会在字符串两边加上双引号,并且字符串中间的双引号前面也会加上双引号,那这个json字符串就惨不忍睹了(不过,此时好像用fastjson能转过来,见:redis发布订阅)。
public class StringRedisSerializer implements RedisSerializer<String> {
private final Charset charset;
public StringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
public StringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
@Override
public String deserialize(@Nullable byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(@Nullable String string) {
return (string == null ? null : string.getBytes(charset));
}
}
配置RedisTemplate
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
<!--Lettuce是 一 个 基 于 Netty的 NIO方 式 处 理 Redis的 技 术 -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
RedisConfig
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
RedisSerializer<String> stringRedisSerializer = RedisSerializer.string();
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
RedisService
package com.zzhua.blog.config.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Component
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/* key 是否存在 */
public Boolean existKey(String key) {
return redisTemplate.hasKey(key);
}
/* 设置 key 失效时间 */
public Boolean expireKey(String key, long timeInSeconds) {
return redisTemplate.expire(key, timeInSeconds, TimeUnit.SECONDS);
}
/* 移除 key */
public Boolean removeKey(String key) {
return redisTemplate.delete(key);
}
/* 移除多个 key */
public Boolean removeKeys(Collection<String> keys) {
return redisTemplate.delete(keys) > 0;
}
public Long incr(String key) {
return redisTemplate.opsForValue().increment(key);
}
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
public void set(String key, Object obj, long timeInSeconds) {
redisTemplate.opsForValue().set(key, obj, timeInSeconds, TimeUnit.SECONDS);
}
public void set(String key, Object obj) {
redisTemplate.opsForValue().set(key, obj);
}
public <T> T get(String key) {
return (T)redisTemplate.opsForValue().get(key);
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)