源码剖析SpringBoot集成Redis中的序列化问题
前言
在之前SpringBoot1.x的版本中,集成Redis实现自定义的序列化都是通过定制RedisTemplate来实现的,但是最近在SpringBoot2.x的版本中发现这个方法好像行不通了,于是就通过源码分析了一下,在这里做一下记录,分享给有需要的小伙伴们。
SpringBoot集成Redis
首先,从SpringBoot集成Redis的过程来讲。默认情况下,SpringBoot有自己的缓存机制。
@EnableCaching注解
在主程序类上加上这个注解来开启缓存机制是第一步,注解内容如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({CachingConfigurationSelector.class})
public @interface EnableCaching {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default 2147483647;
}
它会导入一个 CachingConfigurationSelector类,这个选择器会根据当前环境检测一系列的 CacheConfiguration类,默认会加载SimpleCacheConfiguration。
在集成了Redis之后,它便会加载 RedisCacheConfiguration,在这个类中它进行了一项自动配置,如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
// 注意这个自动配置类
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
这个自动配置类会根据当前环境进行一系列的配置。
RedisCacheConfiguration
这个类是整个机制中的一个核心类,不过有两个,注意是这个包 org.springframework.boot.autoconfigure.cache下面的。它向容器中注册了一个至关重要的Bean RedisCacheManager。它负责创建、管理和控制所有的RedisCache。
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
// 下面的determineConfiguration方法用来决定配置信息。
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
CacheProperties cacheProperties,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
// 这个类根据缓存属性以及当前的类加载器调用本类的方法创建出了一个 RedisCacheConfiguration对象。继续往下看
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
// 在这里调用了 RedisCacheConfiguration这个类的方法首先创建出了一个默认的缓存配置。具体的后续再看。。。
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
/*
然后通过 SerializationPair 这个类通过 JdkSerializationRedisSerializer这个Jdk的序列化方法
创建出了一个 SerializationPair(这个具体是干嘛的后面会再说)。
接着指定了这个 SerializationPair 用来序列化 value
*/
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
// 下面代码省略。。。
return config;
}
看到这里应该对整体有一个初步的了解了:
- @EnableCahing导入了一个 CachingConfigurationSelector 用来选择加载哪个CacheConfiguration
- 集成Redis之后,容器加载了 RedisCaheConfiguration。
- RedisCaheConfiguration 这个类向容器中注册了一个Bean,RedisCacheManager
- 在创建这个RedisCacheManager的时候创建了另一个包下的 RedisCacheConfiguration作为一些配置
- 在创建这个configuration的过程中指定了 JdkSerializationRedisSerializer 作为默认序列化value的方法。
所以,我们要想自定义我们自己的序列化方法,只需要针对这个 RedisCacheConfiguration 类进行修改即可。
@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
// redis提供的转json的实现类。注意这里的Object需要替换成你具体要序列化的类,否则在读缓存的时候会报错。
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofDays(30));
return configuration;
}
}
看到这里你可能会不解,难道配置在config中的这个接口就一定会被用来对value进行序列化嘛?如果你有耐心有兴趣的话,就接着看下面的源码。
Redis的put缓存流程
首先,对上面提到的一些问题进行补充。
前面提到了在创建 RedisCacheConfiguration的过程中,会首先创建一个默认的configuration。来继续深入:
public static RedisCacheConfiguration defaultCacheConfig() {
return defaultCacheConfig(null);
}
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
// 可以看到在这里,创建出了两个 SerializationPari分别作为参数来创建RedisCacheConfiguration
SerializationPair.fromSerializer(RedisSerializer.string()),
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
上面的两个 SerializationPair 一个是通过 string()方法,一个是通过类加载机制来创建。继续看
public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);
static RedisSerializer<String> string() {
return StringRedisSerializer.UTF_8;
}
static RedisSerializer<Object> java(@Nullable ClassLoader classLoader) {
return new JdkSerializationRedisSerializer(classLoader);
}
可以很清楚的看到,其中一个是返回了字符串序列化器,一个是返回了JdkSerializationRedisSerializer。好,现在知道创建了两个什么类型的 SerializationPair之后,来继续看这两个 SerializationPair用到哪去了。回到上面RedisCacheConfiguration的构造方法:
private RedisCacheConfiguration(Duration ttl, Boolean cacheNullValues, Boolean usePrefix, CacheKeyPrefix keyPrefix,
SerializationPair<String> keySerializationPair, SerializationPair<?> valueSerializationPair,
ConversionService conversionService) {
this.ttl = ttl;
this.cacheNullValues = cacheNullValues;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
this.valueSerializationPair = (SerializationPair<Object>) valueSerializationPair;
this.conversionService = conversionService;
}
可以很清楚的看到,StringRedisSerializer作为了key的序列化器而JdkSerializationRedisSerializer作为了value的序列化器。
Cache的put操作
缓存机制中,CacheManager负责创建、控制和管理一系列的Cache,而Cache就负责创建、管理和控制Entry。所以我们来看RedisCache中的put操作。
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = preProcessCacheValue(value);
if (!isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format(
"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
name));
}
// 重点在这,它调用了cacheWriter的put方法进行操作。
cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
}
通过上面代码可以知道,RedisCache是调用了cacheWriter类的put方法来进行真正的操作的。这个CacheWriter前面忘记提了,这里补充一下,RedisManager在创建RedisCache的时候会把自己的CacheWriter传过去。这个CacheWriter是一个接口,有一个实现类是 DefaultCacheWriter。再往这里面看它就是调用了RedisConnection方法的实现了,就是命令行了,这里就不说那么多了。
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
return new RedisCache(name, cacheWriter, cacheConfig != null ? cacheConfig : defaultCacheConfig);
}
ok,回到上面的put方法。可以看到,在调用cacheWriter的put方法之前,分别对key和value进行了预处理,而这个预处理是什么呢,很简单,就是进行序列化,如下:
private byte[] createAndConvertCacheKey(Object key) {
return serializeCacheKey(createCacheKey(key));
}
protected byte[] serializeCacheKey(String cacheKey) {
return ByteUtils.getBytes(cacheConfig.getKeySerializationPair().write(cacheKey));
}
protected byte[] serializeCacheValue(Object value) {
if (isAllowNullValues() && value instanceof NullValue) {
return BINARY_NULL_VALUE;
}
return ByteUtils.getBytes(cacheConfig.getValueSerializationPair().write(value));
}
就是从cacheConfig中获得之前所设定好的SerializationPair分别对key和value进行序列化。
总结
因本人水平经验有限,可能存在各方面的错误,欢迎各位大佬进行指教与讨论留言。有不明白的地方也可以留言或者私信我。