美文网首页
SpringCache踩坑记

SpringCache踩坑记

作者: r09er | 来源:发表于2020-01-08 22:13 被阅读0次

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,需要提供一个序列化器,防止反序列的时候出错.

相关文章

  • SpringCache踩坑记

    SpringCache配合Redis使用缓存. 完整配置在最后 目的:使用注解形式优雅地序列化数据到redis中,...

  • Android Material Design 踩坑记(2)

    Android Material Design 踩坑记(1) CoordinatorLayout Behav...

  • Deepin使用踩坑记

    1. 前言 很喜欢Deepin,奈何坑太多,不过不怕,踩过去~ 2. 踩坑记 2.1 Deepin重启后文件管理器...

  • SpringStreaming+Kafka

    摘自 :Spark踩坑记——Spark Streaming+Kafka [TOC] SpringStreaming...

  • 原生App植入React Native 踩坑记

    原生App植入React Native 踩坑记 为什么我踩坑了有一个需求要去可以在当前工程的feature/RN ...

  • [ANR Warning]onMeasure time too

    ConstraintLayout 踩坑记一次封装组合控件时的坑,我才用了集成 ConstraintLayout 来...

  • IdentityServer 部署踩坑记

    IdentityServer 部署踩坑记 Intro 周末终于部署了 IdentityServer 以及 Iden...

  • 踩坑记

    1、android自签名证书Glide加载不出图片 关于https中自签名证书的介绍以及OkHttp中解决自签名证...

  • 踩坑记

    6月初,看到广西龙脊梯田有个疏秧节,很是心动!我十几年前就去过龙脊,当时觉得整片的梯田又美又壮观,壮族风情浓厚,就...

  • 踩坑记

    该篇文章记录踩过的坑 uglifyjs-webpack-plugin压缩代码报punc (()错误网上查资料,说是...

网友评论

      本文标题:SpringCache踩坑记

      本文链接:https://www.haomeiwen.com/subject/qxleactx.html