学习链接

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)

}
  • 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);
    }

}