前言
在上一篇文章中,我們完成了SpringBoot整合Redis進行資料快取管理的工作,但快取管理的實體類資料使用的是JDK序列化方式(如下圖所示),不便於使用視覺化管理工具進行檢視和管理。
接下來分別針對基於API的Redis快取實現和基於註解的Redis快取實現中的資料序列化機制進行介紹,並自定義JSON格式的資料序列化方式進行資料快取管理。
基於API的Redis快取實現——自定義RedisTemplate
1、Redis API預設序列化方式原始碼解析
基於API的Redis快取實現是使用RedisTemplate模板進行資料快取操作的,檢視RedisTemplate的原始碼資訊:
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware { private boolean enableTransactionSupport = false;
private boolean exposeConnection = false;
private boolean initialized = false;
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader; // 聲明瞭key、value的各種序列化方式,初始值為空
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
... /*
* 進行預設序列化方式設定,設定為JDK序列化方式
* (non-Javadoc)
* @see org.springframework.data.redis.core.RedisAccessor#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() { super.afterPropertiesSet(); boolean defaultUsed = false; if (defaultSerializer == null) { defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
} if (enableDefaultSerializer) { if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
} if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
} if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<>(this);
} initialized = true;
}
...
}
從上述RedisTemplate核心原始碼可以看出,在RedisTemplate內部聲明瞭快取資料key、value的各種序列化方式,各種初始值都為空;在afterPropertiesSet()方法中,判斷如果預設序列化引數defaultSerializer為空,則將資料的預設序列化方式設定為JdkSerializationRedisSerializer。
根據上述原始碼資訊可得出以下兩個重要結論:
(1)使用RedisTemplate進行Redis資料快取操作時,內部預設使用的是JdkSerializationRedisSerializer序列化方式,所以進行資料快取的實體類必須實現JDK自帶的序列化介面(例如Serializable);
(2)使用RedisTemplate進行Redis資料快取操作時,如果自定義了快取序列化方式defaultSerializer,那麼將使用自定義的序列化方式。
另外,在RedisTemplate類的原始碼中,看到的快取資料key、value的各種序列化型別都是RedisSerializer。進入RedisSerializer檢視RedisSerializer支援的序列化方式:
可以看到,RedisSerializer是一個Redis序列化介面,預設有6個實現類,這6個實現類代表了6種不同的資料序列化方式。其中,JdkSerializationRedisSerializer是JDK自帶的,也是RedisTemplate內部預設使用的序列化方式,開發者可以根據需要選擇其他支援的序列化方式(例如JSON方式)。
2、自定義RedisTemplate序列化機制
在專案中引入Redis依賴後,SpringBoot提供的RedisAutoConfiguration自動配置會生效。開啟RedisAutoConfiguration類,檢視內部原始碼中關於RedisTemplate的定義方式:
package org.springframework.boot.autoconfigure.data.redis; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate; /**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Data's Redis support.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Christian Dupuis
* @author Christoph Strobl
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Marco Aust
* @author Mark Paluch
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration { @Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
} ... }
從上述RedisAutoConfiguration核心原始碼中可以看出,在Redis自動配置類中,通過Redis連線工廠RedisConnectionFactory初始化了一個RedisTemplate;在該方法上方添加了一個@ConditionalOnMissingBean註解(顧名思義,當某個Bean不存在時生效),用來表明如果開發者自定義了一個名為redisTemplate的Bean,那麼該預設初始化的RedisTemplate就不會生效。
如果要使用自定義序列化方式的RedisTemplate進行資料快取操作,可以參考上述核心程式碼建立一個名為redisTemplate的Bean元件,並在該元件中設定對應的序列化方式即可。
接下來,在專案中建立名為com.hardy.springbootdatacache.config的包,在該包下建立一個Redis自定義配置類RedisConfig,並按照上述思路自定義名為redisTemplate的Bean元件:
package com.hardy.springbootdatacache.config; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; /**
* @Author: HardyYao
* @Date: 2021/6/24
*/
@Configuration
public class RedisConfig { @Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// ֵ使用JSON格式序列化物件,對快取資料key和value進行轉換
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
// 解決查詢快取轉換異常的問題
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 設定RedisTemplate模板API的序列化方式為JSON
template.setDefaultSerializer(jacksonSeial);
return template;
} }
上述程式碼通過@Configuration註解定義了一個RedisConfig配置類,並使用@Bean註解注入了一個預設名稱為方法名的redisTemplate的Bean元件(注意:該Bean元件名稱必須是redisTemplate)。在定義的Bean元件中,自定義了一個RedisTemplate,使用自定義的Jackson2JsonRedisSerializer資料序列化方式;在定製序列化方式中,定義了一個ObjectMapper用於進行資料轉換設定。
3、效果測試
啟動專案,通過瀏覽器訪問:http://localhost:8080/api/findCommentById?id=2(連續訪問三次),檢視網頁返回資訊及控制檯訊息:
根據控制檯列印訊息可知,執行findById()方法正確查詢出了使用者評論資訊Comment,重複進行同樣的查詢操作,資料庫也不會重複執行SQL語句,這表明定製的Redis快取生效了。
使用Redis客戶端視覺化管理工具Redis Desktop Manager檢視快取資料:
執行findById()方法查詢到的使用者評論資訊Comment正確儲存到了Redis快取庫中,且快取到Redis服務的資料已經使用了JSON格式的資料儲存展示,檢視和管理也十分方便,這說明自定義的Redis API模板工具RedisTemplate生效了。
基於註解的Redis快取實現——自定義RedisCacheManager
剛剛針對基於API方式的RedisTemplate進行了自定義序列化方式的改進,從而實現了JSON序列化方式快取資料,但是這種自定義的RedisTemplate對於基於註解的Redis快取來說,是沒有作用的。
接下來,針對基於註解的Redis快取機制和自定義序列化方式進行講解。
1、Redis註解預設序列化機制
開啟SpringBoot整合Redis元件提供的快取自動配置類RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),檢視該類的原始碼資訊,其核心程式碼如下:
package org.springframework.boot.autoconfigure.cache; import java.util.LinkedHashSet;
import java.util.List; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheProperties.Redis;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheManager.RedisCacheManagerBuilder;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair; @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration { @Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
if (cacheProperties.getRedis().isEnableStatistics()) {
builder.enableStatistics();
}
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) {
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
} private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
// 預設也是使用JdkSerializationRedisSerializer作為序列化方式
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
} }
從上述核心原始碼可看出,同RedisAutoConfiguration原始碼(其中定義的RedisTemplate)類似,RedisCacheConfiguration內部同樣通過Redis連線工廠RedisConnectionFactory定義了一個快取管理器RedisCacheManager;同時定製RedisCacheManager時,也預設使用了JdkSerializationRedisSerializer序列化方式。
如果想要使用自定義序列化方式的RedisCacheManager進行資料快取操作,可以參考上述核心原始碼建立一個名為cacheManager的Bean元件,並在該元件中設定對應的序列化方式即可。
注意:在SpringBoot 2.X版本中,RedisCacheManager是單獨進行構建的。因此,在SpringBoot 2.X版本中,對RedisTemplate進行自定義序列化機制構建後,仍然無法對RedisCacheManager內部預設序列化機制進行覆蓋(這也就解釋了基於註解的Redis快取實現仍然會使用JDK預設序列化機制的原因),想要基於註解的Redis快取實現也是用自定義序列化機制。想要自定義RedisCacheManager。
2、自定義RedisCacheManager
在專案的Redis配置類RedisConfig,按照上一步分析的定製方法自定義名為cacheManager的Bean元件:
package com.hardy.springbootdatacache.config; import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; /**
* @Author: HardyYao
* @Date: 2021/6/24
*/
@Configuration
public class RedisConfig { ... @Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分別建立String和JSON格式序列化物件,對快取資料key和value進行轉換
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSerial = new Jackson2JsonRedisSerializer(Object.class);
// 解決查詢快取轉換異常問題
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSerial.setObjectMapper(om);
// 定製快取資料序列化方式及時效
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSerial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
} }
上述程式碼中,在RedisConfig配置類中使用@Bean註解注入了一個預設名稱為方法名的cacheManager元件。在定義的Bean元件中,通過RedisCacheConfiguration對快取資料的key和value分別進行了序列化方式的定製,其中快取資料的key定製為StringRedisSerializer(即String格式),而value定製了Jackson2JsonRedisSerializer(即JSON格式),同時還是用entryTtl(Duration.ofDays(1))方式將快取資料有效期設定為1天。
完成基於註解的Redis快取管理器RedisCacheManager定製後,可以對該快取管理器的效果進行測試。(記得要開啟SpringBoot基於註解的快取管理支援,即在啟動類上新增@EnableCaching註解。另外,使用自定義序列化機制的RedisCacheManager測試時,實體類可以不用實現序列化介面)。
啟動專案,通過瀏覽器訪問:http://localhost:8080/findCommentById?id=2(連續訪問三次),檢視網頁返回資訊及控制檯訊息:
根據控制檯列印訊息可知,執行findById()方法正確查詢出了使用者評論資訊Comment,重複進行同樣的查詢操作,資料庫也不會重複執行SQL語句,這表明定製的Redis快取生效了。
使用Redis客戶端視覺化管理工具Redis Desktop Manager檢視快取資料:
可以看到使用者評論資訊Comment正確儲存到了Redis快取庫中,且快取到Redis服務的資料已經使用了JSON格式的資料儲存展示,這說明自定義的基於註解的Redis快取管理器RedisCacheManager生效了。