阅读 555

SpringDataCache踩坑记

SpringDataCache配合Redis使用缓存.

完整配置在最后

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

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

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

常见的坑

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

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

1.1.1 错误提示

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.xx
复制代码

1.1.2 错误原因解析

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

1.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();

完整配置代码

添加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>
复制代码

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