SpringCache踩坑記

SpringCache配合Redis使用緩存.

完整配置在最后

目的:使用注解形式優(yōu)雅地序列化數據到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生成規(guī)則

參考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.構造中創(chuàng)建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)));
    }

//常規(guī)的反序列化操作
    @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閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異泌射,居然都是意外死亡,警方通過查閱死者的電腦和手機鬓照,發(fā)現死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門熔酷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人豺裆,你說我怎么就攤上這事拒秘。” “怎么了臭猜?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵躺酒,是天一觀的道長。 經常有香客問我蔑歌,道長羹应,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任次屠,我火速辦了婚禮量愧,結果婚禮上,老公的妹妹穿的比我還像新娘帅矗。我一直安慰自己,他們只是感情好煞烫,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布浑此。 她就那樣靜靜地躺著,像睡著了一般滞详。 火紅的嫁衣襯著肌膚如雪凛俱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天料饥,我揣著相機與錄音蒲犬,去河邊找鬼。 笑死岸啡,一個胖子當著我的面吹牛原叮,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼奋隶,長吁一口氣:“原來是場噩夢啊……” “哼擂送!你這毒婦竟也來了?” 一聲冷哼從身側響起唯欣,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤嘹吨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后境氢,有當地人在樹林里發(fā)現了一具尸體蟀拷,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年萍聊,在試婚紗的時候發(fā)現自己被綠了问芬。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡脐区,死狀恐怖愈诚,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情牛隅,我是刑警寧澤炕柔,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站媒佣,受9級特大地震影響匕累,放射性物質發(fā)生泄漏。R本人自食惡果不足惜默伍,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一欢嘿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧也糊,春花似錦炼蹦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至钞馁,卻和暖如春虑省,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背僧凰。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工探颈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人训措。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓伪节,卻偏偏與公主長得像光羞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子架馋,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容