SpringBoot 2.X中使用緩存註解時,發現某些配置方面與SpringBoot 1.X有些異同,這裏做爲第一篇博客記錄下來。java
1.建立SpringBoot 2.0.1 工程,引入spring-boot-starter-data-redis依賴redis
2.配置redis的鏈接屬性spring
spring: redis: password: host: localhost port: 6379 cache: type: redis
spring.cache.type 指定註解緩存使用redis緩存
3.添加@EnableCaching開啓緩存註解springboot
@EnableCaching @SpringBootApplication public class SpringBoot2Application { public static void main(String[] args) { SpringApplication.run(SpringBoot2Application.class, args); } }
4.使用緩存註解app
通用屬性解釋:dom
value屬性:要使用緩存的名稱spring-boot
key屬性:使用SpEL表達式自定義緩存Key,例如:#name—以參數name做爲自定義緩存Key,#result.name—以返回值結果的name屬性做爲自定義緩存Key源碼分析
(1)@Cacheable註解ui
若是沒有緩存則會執行方法並將返回值緩存,若是有緩存時,不會執行方法而是直接返回緩存中的值
@GetMapping @Cacheable(value = "users", key = "#name") public User getByName(String name) { log.info("get user info..."); User user = new User(); user.setName(name); return user; }
第一次發起GET請求/users?name=123456時,會輸出get user info...日誌而且查看redis會發現如下結果
再次發起一個GET請求/users?name=123456,並無日誌輸出,可見有緩存時並不會調用方法。
(2)@CachePut註解
無論有沒有緩存都會執行方法並將結果緩存起來
@PostMapping @CachePut(value = "users", key = "#result.name") public User add(String name) { User user = new User(); user.setId(1); user.setName(name); if (userRepository.save(user)) { log.info("用戶:%s 保存成功!\n", user); } return user; }
發起POST請求/users?name=123456,可見每次都會打印保存成功的日誌而且將最新的結果緩存起來
(3)@CacheEvict註解
移除指定緩存
@DeleteMapping @CacheEvict(value = "users", key = "#name") public void deleteByName(String name) { log.info("delete user by name :{}", name); }
發起DELETE請求/users?name=123456,可見每次都會打印刪除日誌而且刪除緩存
注意:
a.User對象須要實現序列化接口
b.只有@CacheEvict註解的方法返回值能夠爲void
5.使用@CacheConfig註解進一步簡化
@Slf4j @CacheConfig(cacheNames = "users") @RestController @RequestMapping("/users") public class UserController { private final UserRepository userRepository; @Autowired public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @PostMapping @CachePut public User add(String name) { User user = new User(); user.setName(name); if (userRepository.save(user)) { log.info("用戶:%s 保存成功!\n", user); } return user; } @GetMapping @Cacheable public User getByName(String name) { log.info("get user info..."); User user = new User(); user.setName(name); return user; } @DeleteMapping @CacheEvict public void deleteByName(String name) { log.info("delete user by name :{}", name); } }
使用@CacheConfig聲明類下的緩存註解的value默認是"users",讓代碼更簡潔、優雅,效果與上面同樣。
6.指定序列化器
上面就已經簡單的使用了緩存註解,實際上還有不少註解屬性沒有介紹到,由於並非本篇的重點,有興趣的小夥伴能夠本身進行拓展,好比,條件化緩存,XML方式對源碼中的方法進行緩存,分組緩存等特性。上面當咱們去redis查看value值時,咱們一臉懵逼根本看不懂存的是啥東西。下面讓咱們自定義序列化器來讓緩存的value值透明化,這裏的自定義配置也是SpringBoot 2.X 與SpringBoot 1.X不同的地方。
@Bean public RedisCacheConfiguration redisCacheConfiguration() { return RedisCacheConfiguration .defaultCacheConfig() .serializeKeysWith( RedisSerializationContext .SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith( RedisSerializationContext .SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); }
當咱們添加以上配置後,使用緩存註解時會使用StringRedisSerializer對Key進行序列化,使用GenericJackson2JsonRedisSerializer對Value進行反序列化。
此時從新啓動咱們刪除原有的緩存後再次發起一個GET請求/users?name=123456
與以前效果同樣,不同的在於咱們終於能看懂緩存中Value裝的是啥玩意兒了,以下圖:
class用於在反序列化時將其由JSON數據反序列化爲User實體
id與name都是User實體中的屬性
7.源碼分析
SpringBoot 1.X 中自定義序列化器一般是聲明一個RedisCacheManager並在其構造中傳一個RedisTemplate,接着對RedisTemplate配置自定義序列化器就可達到自定義序列化器的目的。可是SpringBoot 2.X 中你會發現RedisCacheManager的構造方法徹底變樣了,再也不是依賴RedisTemplate。下面來走源碼分析一波。
首先,咱們進到RedisCacheConfiguration看一波
//默認聲明的RedisCacheManager @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) { //與Spring Boot 1.X 中RedisCacheManager的構造方式不一樣 RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader())); List<String> cacheNames = this.cacheProperties.getCacheNames(); if (!cacheNames.isEmpty()) { builder.initialCacheNames(new LinkedHashSet(cacheNames)); } return (RedisCacheManager)this.customizerInvoker.customize(builder.build()); }
SpringBoot 的契約優於配置的特性已經幫咱們配了一個默認的RedisCacheManager
//決定使用的RedisCacheConfiguration配置 private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(ClassLoader classLoader) { //若是有自定義的RedisCacheConfiguration則使用自定義的 if (this.redisCacheConfiguration != null) { return this.redisCacheConfiguration; //沒有則使用默認配置 } else { Redis redisProperties = this.cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig(); //默認使用JDK自帶的序列化器 config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader))); if (redisProperties.getTimeToLive() != null) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
能夠發現若是沒有找到RedisCacheConfiguration Bean對象 則使用默認配置中的序列化器—JDK自帶的序列化器,若是有就使用自定義的RedisCacheConfiguration,這就能夠解釋爲何咱們想使用自定義的序列化器須要聲明一個RedisCacheConfiguration Bean對象了。
8.Jackson序列化器
Jackson序列化器其實有兩個,Jackson2JsonRedisSerializer和咱們上面使用的GenericJackson2JsonRedisSerializer。
若是使用Jackson2JsonRedisSerializer在反序列化時會遇到問題,由於沒有具體泛型或泛型爲Object時,會將緩存中的數據反序列化爲LinkedHashMap,而咱們須要的是User對象,所以就會拋出一個異常。
@Bean public RedisCacheConfiguration redisCacheConfiguration() { return RedisCacheConfiguration .defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) //若是將Object.class替換爲User.class也能夠暫時解決問題可是這就不是一個通用的序列化器了 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class))); }
Jackson2JsonRedisSerializer<T>源碼
public T deserialize(@Nullable byte[] bytes) throws SerializationException { if (SerializationUtils.isEmpty(bytes)) { return null; } else { try { return this.objectMapper.readValue(bytes, 0, bytes.length, this.javaType); } catch (Exception var3) { throw new SerializationException("Could not read JSON: " + var3.getMessage(), var3); } } }
使用Jackson2JsonRedisSerializer反序列化時的轉換異常
java.lang.ClassCastException: java.base/java.util.LinkedHashMap cannot be cast to com.springboot2.domain.User
而使用GenericJackson2JsonRedisSerializer就能夠避免這種狀況。