SpringCache配合Redis使用缓存.
完整配置在最后
目的:使用注解形式优雅地序列化数据到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生成规则
参考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,需要提供一个序列化器,防止反序列的时候出错.