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 projectLocalDateTime
andLocalDate
, so there are more points and configuration items to be aware of
The pit of common
1.1 using theJackson2JsonRedisSerializer
Configure 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 usedLocalDateTime
orLocalDate
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 serializerObjectMapper
object
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 UseGenericJackson2JsonRedisSerializer
Rather 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 one
NullValueSerializer
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 UseObjectMapper
You 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
3registerNullValueSerializer
Function 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.