SpringCache踩坑记

SpringCache配合Redis使用缓存.

完整配置在最后

目的:使用注解形式优雅地序列化数据到redis中,并且数据都是可读的json格式

为了达到以上目的,在SpringCache的使用过程中,需要自定义Redis的Serializer和Jackson的ObjectMapper,而且非常多坑.

由于项目中使用了Java版本为JDK8,并且整个项目中关于时间的操作类全都是LocalDateTimeLocalDate,所以有更多需要注意的点和配置项

常见的坑

1 使用了Jackson2JsonRedisSerializer配置Redis序列化器

这个类名看着就是是Jackson用于redis序列化的,然而...

1.1错误提示

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.xx

1.2错误原因解析

当对象序列成json数据,再进行反序列的时候,Jackson并不知道json数据原本的Java对象是什么,所以都会使用LinkedHashMap进行映射,这样就能映射所有的对象类型,但是这样就会导致序列化时候出现异常.

1.3解决办法

使用GenericJackson2JsonRedisSerializer

@Bean
public RedisSerializer<Object> redisSerializer() {
...略
return GenericJackson2JsonRedisSerializer;
}

2 缓存对象使用了LocalDateTime或者LocalDate

2.1错误提示

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

2.2错误原因解析

因为LocalDateTime没空构造,无法反射进行构造,所以会抛出异常.(如果自定义的对象没有提供默认构造,也会抛出这个异常)

2.3解决办法

  • 1.局部使用注解
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
  • 2.使用全局的配置,注入Redis序列化器

示例代码

@Bean
public RedisSerializer<Object> redisSerializer() {

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
    //不适用默认的dateTime进行序列化,使用JSR310的LocalDateTimeSerializer
    objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
        //重点,这是序列化LocalDateTIme和LocalDate的必要配置,由Jackson-data-JSR310实现
    objectMapper.registerModule(new JavaTimeModule());
    //必须配置,有兴趣参考源码解读
    objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
    return new GenericJackson2JsonRedisSerializer(objectMapper);

}

如果没有JavaTimeModule这个类,需要添加jackson-data-jsr310的依赖,不过在springboot-starter-web模块已经包含了,所以理论上不需要单独引入

<dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
      <version>2.10.1</version>
      <scope>compile</scope>
</dependency>

3 使用配置Redis序列化器的时候使用的JacksonAutoConfiguration自动注入的ObjectMapper对象

即不new ObjectMapper(),而是通过属性或者参数注入

使用了这个对象的后果是灾难性的,会改变AbstractJackson2HttpMessageConverter的中的ObjectMapper对象,导致json响应数据异常

3.1错误提示

不出导致出错,但是正常的JSON响应体就会变得不再适用

3.2 错误原因解析

使用了SpringBoot自动注入的ObjectMapperBean对象,然后又对这个对象进行了配置,因为这个对象默认是为json响应转换器`AbstractJackson2HttpMessageConverter``服务的,这个bean的配置和缓存的配置会略有不同.

3.3 解决办法

在定义Redis序列号器的时候new ObjectMapper();

完整配置代码

1.添加Spring-cache,redis依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.配置Redis序列化器

@Configuration
public class RedisConfig {



   /**
     * 自定义redis序列化的机制,重新定义一个ObjectMapper.防止和MVC的冲突
     *
     * @return
     */
    @Bean
    public RedisSerializer<Object> redisSerializer() {

        ObjectMapper objectMapper = new ObjectMapper();
        //反序列化时候遇到不匹配的属性并不抛出异常
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //序列化时候遇到空对象不抛出异常
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        //反序列化的时候如果是无效子类型,不抛出异常
        objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
        //不使用默认的dateTime进行序列化,
        objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
                //使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类
        objectMapper.registerModule(new JavaTimeModule());
        //启用反序列化所需的类型信息,在属性中添加@class
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        //配置null值的序列化器
        GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
        return new GenericJackson2JsonRedisSerializer(objectMapper);


    }


    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {


        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setDefaultSerializer(redisSerializer);
        template.setValueSerializer(redisSerializer);
        template.setHashValueSerializer(redisSerializer);
        template.setKeySerializer(StringRedisSerializer.UTF_8);
        template.setHashKeySerializer(StringRedisSerializer.UTF_8);
        template.afterPropertiesSet();
        return template;
    }
    
}    

3.配置SpringCache继承CachingConfigurerSupport

重写KeyGenerator方法该方法是缓存到redis的默认Key生成规则

参考redis缓存key的设计方案,这边将根据类名,方法名和参数生成key

@Configuration
@EnableCaching
class CacheConfig extends CachingConfigurerSupport{
    
    @Bean
    public CacheManager cacheManager(@Qualifier("redissonConnectionFactory") RedisConnectionFactory factory, RedisSerializer<Object> redisSerializer) {
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(getRedisCacheConfigurationWithTtl(60, redisSerializer))
                .build();
        return cacheManager;
    }

    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer minutes, RedisSerializer<Object> redisSerializer) {

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration
                .prefixKeysWith("ct:crm:")
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .entryTtl(Duration.ofMinutes(minutes));

        return redisCacheConfiguration;
    }
    
    @Override
    public KeyGenerator keyGenerator() {
        // 当没有指定缓存的 key时来根据类名、方法名和方法参数来生成key
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName())
                    .append(':')
                    .append(method.getName());
            if (params.length > 0) {
                sb.append('[');
                for (Object obj : params) {
                    if (obj != null) {
                        sb.append(obj.toString());
                    }
                }
                sb.append(']');
            }
            return sb.toString();
        };
    }
}

源码解读

1为什么使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer

通过空构造进行初始化步骤

  • 1.无参构造调用一个参数的构造
  • 2.构造中创建ObjectMapper,并且设置了一个NullValueSerializer
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
  • 3.ObjectMapper设置包含类信息

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY)

源码

public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {

    private final ObjectMapper mapper;

    public GenericJackson2JsonRedisSerializer() {
        this((String) null);
    }

    public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {

        this(new ObjectMapper());
        //这个步骤非常重要,关乎反序列的必要设置
        registerNullValueSerializer(mapper, classPropertyTypeName);

        if (StringUtils.hasText(classPropertyTypeName)) {
            mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
        } else {
            mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
        }
    }
    //有参构造,只是把对象赋值了,但是没有配置空构造的两个方法
    public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {

        Assert.notNull(mapper, "ObjectMapper must not be null!");
        this.mapper = mapper;
    }
//反序列化时候的必要操作,注册null值的序列化器
    public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {

    
        objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
    }

//常规的反序列化操作
    @Nullable
    public <T> T deserialize(@Nullable byte[] source, Class<T> type) throws SerializationException {

        Assert.notNull(type,
                "Deserialization type must not be null! Please provide Object.class to make use of Jackson2 default typing.");

        if (SerializationUtils.isEmpty(source)) {
            return null;
        }

        try {
            return mapper.readValue(source, type);
        } catch (Exception ex) {
            throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
        }
    }
//null值序列化器,目的是防止反序列化造成的异常出错
    private static class NullValueSerializer extends StdSerializer<NullValue> {

        private static final long serialVersionUID = 1999052150548658808L;
        private final String classIdentifier;

        NullValueSerializer(@Nullable String classIdentifier) {

            super(NullValue.class);
            this.classIdentifier = StringUtils.hasText(classIdentifier) ? classIdentifier : "@class";
        }

        @Override
        public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider)
                throws IOException {

            jgen.writeStartObject();
            jgen.writeStringField(classIdentifier, NullValue.class.getName());
            jgen.writeEndObject();
        }
    }
}

serialize方法,在Jackson在序列化对象的时候,插入了一个字段@class.这个字段就是用来记录反序列化时Java的全限定类名

redis缓存中的数据

{
//插入了一个额外的字段用于标识对象的具体Java类
  "@class": "com.ndltd.admin.common.model.sys.entity.SysUserTokenEntity",
  "userId": 1112649436302307329,
  "token": "fd716b735c0159c9a25cf20fc4a1f213",
  "expireTime": [
    "java.util.Date",
    1578411896000
  ],
  "updateTime": [
    "java.util.Date",
    1578404696000
  ]
}

2 为什么使用ObjectMapper的时候需要配置一堆的东西

ObjectMapper默认会严格按照Java对象和Json数据一一匹配,但是又由于需要提供一个额外的@class属性,所以反序列化的时候就会出错,所以需要配置

ObjectMapper objectMapper = new ObjectMapper();
//反序列化时候遇到不匹配的属性并不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//序列化时候遇到空对象不抛出异常
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//反序列化的时候如果是无效子类型,不抛出异常
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
//不使用默认的dateTime进行序列化,
objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
//使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类
objectMapper.registerModule(new JavaTimeModule());
//启用反序列化所需的类型信息,在属性中添加@class
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
//配置null值的序列化器
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);

3registerNullValueSerializer方法的作用

// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.

这两句注释是对registerNullValueSerializer的描述

简单翻译:仅仅简单地设置mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)并不会有效果,需要使用嵌入用于反序列化的类型提示。

简单说就是如果value是null,需要提供一个序列化器,防止反序列的时候出错.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容