SpringDataCache works with Redis to use caching.

Complete configuration at the end

Purpose: To serialize data elegantly to Redis using annotations in readable JSON format

In order to achieve the above purpose, the use of SpringCache requires the customization of Redis Serializer and Jackson ObjectMapper.

Since the Java version of the project is JDK8, and the operation classes for time are all in the projectLocalDateTimeandLocalDate, so there are more points and configuration items to be aware of

The pit of common

1.1 using theJackson2JsonRedisSerializerConfigure the Redis serializer

This looks like the class name Jackson used for redis serialization, however…

1.1.1 Error Message

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.xx
Copy the code

1.1.2 Error Cause Analysis

When the objects are sequenced into JSON data and then de-sequenced,Jackson does not know the original Java object of the JSON data, so he uses LinkedHashMap to map all object types, but this will cause an exception during serialization.

1.1.3 Solution

Using GenericJackson2JsonRedisSerializer

@Bean
public RedisSerializer<Object> redisSerializer(a) {... slightlyreturn GenericJackson2JsonRedisSerializer;
}
Copy the code

The 2 cache object is usedLocalDateTimeorLocalDate

2.1 Error Message

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)
Copy the code

2.2 Error Cause Analysis

Because LocalDateTime is not available for construction, an exception is thrown.(This exception is also thrown if a custom object does not provide a default construction.)

2.3 Solutions

  • 1. Use annotations locally
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
Copy the code
  • 2. Inject the Redis serializer using the global configuration

The sample code

@Bean
public RedisSerializer<Object> redisSerializer(a) {

    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);
    // The default dateTime is not used for serialization. Use LocalDateTimeSerializer for JSR310
    objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
		This is the necessary configuration to serialize LocalDateTIme and LocalDate, implemented by jackson-data-jSR310
    objectMapper.registerModule(new JavaTimeModule());
    // Must be configured, interested in source code interpretation
    objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
    return new GenericJackson2JsonRedisSerializer(objectMapper);

}
Copy the code

If the JavaTimeModule class is not available, jackson-data-jSR310 dependencies need to be added, but the Springboot-starter-Web module already includes them, so theoretically there is no need to introduce them separately

<dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jsr310</artifactId>
      <version>2.10.1</version>
      <scope>compile</scope>
</dependency>
Copy the code

3 Automatically injected using the JacksonAutoConfiguration used when configuring the Redis serializerObjectMapperobject

That is, instead of new ObjectMapper(), it is injected via properties or parameters

USES the consequences of this object is disastrous, change AbstractJackson2HttpMessageConverter ObjectMapper of object, resulting in abnormal json response data

3.1 Error Message

No error will result, but the normal JSON response body will no longer be applicable

3.2 Error Cause Analysis

Using the SpringBoot automatic injection ObjectMapperBean object, then the object for the configuration, because this object is the default for the json response converter ` AbstractJackson2HttpMessageConverter ` ` services, the bean The configuration and cache configuration will be slightly different.

3.3 Solutions

New ObjectMapper();

Complete configuration code

Add spring-cache, Redis dependency

<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>
Copy the code

Configure the Redis serializer

@Configuration
public class RedisConfig {



   /** * redefines the redis serialization mechanism to redefine an ObjectMapper. Prevent collisions with MVC * *@return* /
    @Bean
    public RedisSerializer<Object> redisSerializer(a) {

        ObjectMapper objectMapper = new ObjectMapper();
        // Deserialization does not throw an exception if it encounters a mismatched attribute
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // No exception is raised when an empty object is encountered during serialization
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // When deserializing an invalid subtype, no exception is thrown
        objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
        // Do not use the default dateTime for serialization,
        objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
				// Use the serialization class provided by JSR310, which contains a large number of JDK8 time serialization classes
        objectMapper.registerModule(new JavaTimeModule());
        // Enable deserialization of the required type information, add @class to the attribute
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        // Configure the null serializer
        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();
        returntemplate; }}Copy the code

3. Configure SpringCache inheritanceCachingConfigurerSupport

Override KeyGenerator method which is the default Key generation rule cached in Redis

Refer to the redis cache key design scheme, which will generate the key based on the class name, method name and parameter

@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(a) {
        // Generate a key based on the class name, method name, and method parameter when no cached key is specified
        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('] ');
            }
            returnsb.toString(); }; }}Copy the code

The source code interpretation

1 Why to UseGenericJackson2JsonRedisSerializerRather than Jackson2JsonRedisSerializer

The initialization step is performed through an empty construct

  • 1. No-argument construct calls a construct with one parameter
  • 2. Construct ObjectMapper and set oneNullValueSerializer
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
Copy the code
  • 3.ObjectMapper Settings contain class information

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY)

The source code

public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {

	private final ObjectMapper mapper;

	public GenericJackson2JsonRedisSerializer(a) {
		this((String) null);
	}

	public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {

		this(new ObjectMapper());
        // This step is very important for the necessary setup of the antisequence
		registerNullValueSerializer(mapper, classPropertyTypeName);

		if (StringUtils.hasText(classPropertyTypeName)) {
			mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
		} else{ mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY); }}// There are only two methods for assigning an object, but there are no empty constructor methods configured
	public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {

		Assert.notNull(mapper, "ObjectMapper must not be null!");
		this.mapper = mapper;
	}
// Register the null serializer as necessary for deserialization
	public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {

	
		objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
	}

// Normal deserialization operations
	@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); }}// The null value serializer is designed to prevent deserialization exceptions
	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(); }}}Copy the code

The serialize method, when Jackson serializes the object, inserts a field @class. This field is the name of the fully qualified Java class used to record deserialization

Data in redis cache

{
// An additional field is inserted to identify the object's concrete Java class
  "@class": "com.ndltd.admin.common.model.sys.entity.SysUserTokenEntity"."userId": 1112649436302307329."token": "fd716b735c0159c9a25cf20fc4a1f213"."expireTime": [
    "java.util.Date".1578411896000]."updateTime": [
    "java.util.Date".1578404696000]}Copy the code

2 Why to UseObjectMapperYou need to configure a bunch of stuff

By default, ObjectMapper matches Java objects and Json data exactly one by one, but because it needs to provide an extra @class attribute, deserialization will fail, so it needs to be configured

ObjectMapper objectMapper = new ObjectMapper();
// Deserialization does not throw an exception if it encounters a mismatched attribute
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// No exception is raised when an empty object is encountered during serialization
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// When deserializing an invalid subtype, no exception is thrown
objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
// Do not use the default dateTime for serialization,
objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// Use the serialization class provided by JSR310, which contains a large number of JDK8 time serialization classes
objectMapper.registerModule(new JavaTimeModule());
// Enable deserialization of the required type information, add @class to the attribute
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// Configure the null serializer
GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
Copy the code

3registerNullValueSerializerFunction of method

// 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.
Copy the code

This is description of registerNullValueSerializer two comments

Simple translation: simply set mapper. Disable (SerializationFeature. FAIL_ON_EMPTY_BEANS) will not be effective, you need to use the embedded type hinting for deserialization.

If the value is null, a serializer should be provided to prevent unsequence errors.