SpringCache配合Redis使用緩存.
完整配置在最后
目的:使用注解形式優(yōu)雅地序列化數據到redis中,并且數據都是可讀的json格式
為了達到以上目的,在SpringCache的使用過程中,需要自定義Redis的Serializer
和Jackson的ObjectMapper
,而且非常多坑.
由于項目中使用了Java版本為JDK8,并且整個項目中關于時間的操作類全都是LocalDateTime
和LocalDate
,所以有更多需要注意的點和配置項
常見的坑
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自動注入的ObjectMapper
Bean對象,然后又對這個對象進行了配置,因為這個對象默認是為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,需要提供一個序列化器,防止反序列的時候出錯.