前言
在上一篇文章中,我們完成了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生效了。