前言
來(lái)啦老鐵!
筆者學(xué)習(xí)Spring Boot有一段時(shí)間了,附上Spring Boot系列學(xué)習(xí)文章,歡迎取閱、賜教:
- 5分鐘入手Spring Boot;
- Spring Boot數(shù)據(jù)庫(kù)交互之Spring Data JPA;
- Spring Boot數(shù)據(jù)庫(kù)交互之Mybatis;
- Spring Boot視圖技術(shù);
- Spring Boot之整合Swagger;
- Spring Boot之junit單元測(cè)試踩坑;
- 如何在Spring Boot中使用TestNG;
- Spring Boot之整合logback日志;
- Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
- Spring Boot之整合Spring Security: 訪問(wèn)認(rèn)證;
- Spring Boot之整合Spring Security: 授權(quán)管理;
- Spring Boot之多數(shù)據(jù)庫(kù)源:極簡(jiǎn)方案;
- Spring Boot之使用MongoDB數(shù)據(jù)庫(kù)源;
- Spring Boot之多線程、異步:@Async;
- Spring Boot之前后端分離(一):Vue前端;
- Spring Boot之前后端分離(二):后端、前后端集成;
- Spring Boot之前后端分離(三):登錄、登出、頁(yè)面認(rèn)證;
- Spring Boot之面向切面編程:Spring AOP;
- Spring Boot之集成Redis(一):Redis初入門;
- Spring Boot之集成Redis(二):集成Redis;
在上一篇文章Spring Boot之集成Redis(二):集成Redis中,我們一起學(xué)習(xí)了如何使用StringRedisTemplate來(lái)與Redis進(jìn)行交互,但也在文末提到這種方式整體代碼還是比較多的,略顯臃腫,并劇透了另外一種操作Redis的方式:
基于Spring Cache方式操作Redis!
今天的內(nèi)容厲害啦,不僅能學(xué)會(huì)在Spring Boot中更好的使用Redis,還能學(xué)習(xí)Spring Cache!
我們一起撥開(kāi)云霧睹青天吧!
代碼基于上期使用的Git Hub倉(cāng)庫(kù)演進(jìn),歡迎取閱:
整體步驟
- 安裝commons-pool2依賴;
- 修改application.properties配置;
- 學(xué)習(xí)Spring Cache的緩存注解;
- 使用Spring Cache的緩存注解操作Redis;
- Spring Cache + Redis 緩存演示;
- 配置類方式配置和管理Redis緩存;
- 動(dòng)態(tài)緩存有效期的實(shí)現(xiàn);
1. 安裝commons-pool2依賴;
由于我們會(huì)在application.properties配置文件中配置lettuce類型的redis連接池,因此需要引入新的依賴:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
記得安裝一下依賴:
mvn install -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true
2. 修改application.properties配置;
application.properties文件整體樣子:
server.port=8080
# 設(shè)置Spring Cache的緩存類型為redis
spring.cache.type=redis
# Redis數(shù)據(jù)庫(kù)索引(默認(rèn)為0)
spring.redis.database=0
# Redis服務(wù)端的host和port
spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis服務(wù)端的密碼
spring.redis.password=Redis!123
# Redis最大連接數(shù)
spring.redis.pool.max-active=8
# 連接池最大連接數(shù)(使用負(fù)值表示沒(méi)有限制)
spring.redis.lettuce.pool.max-active=8
# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒(méi)有限制)
spring.redis.lettuce.pool.max-wait=-1
# 連接池中的最大空閑連接
spring.redis.lettuce.pool.max-idle=8
# 連接池中的最小空閑連接
spring.redis.lettuce.pool.min-idle=0
# Redis連接超時(shí)時(shí)間,單位 ms(毫秒)
spring.redis.timeout=5000
特別是spring.cache.type=redis這個(gè)配置,指定了redis作為Spring Cache的緩存(還有多種可選的緩存方式,如Simple、none、Generic、JCache、EhCache、Hazelcast等,請(qǐng)讀者有需要自行腦補(bǔ)哈)。
3. 學(xué)習(xí)Spring Cache的緩存注解;
Spring Cache提供了5個(gè)緩存注解,通常直接使用在Service類上,各有其作用:
1. @Cacheable;
@Cacheable 作用在方法上,觸發(fā)緩存讀取操作。
被作用的方法如果緩存中沒(méi)有,比如第一次調(diào)用,則會(huì)執(zhí)行方法體,執(zhí)行后就會(huì)進(jìn)行緩存,而如果已有緩存,那么則不再執(zhí)行方法體;
2. @CachePut;
@CachePut 作用在方法上,觸發(fā)緩存更新操作;
被作用的方法每次調(diào)用都會(huì)執(zhí)行方法體;
3. @CacheEvict;
@CachePut 作用在方法上,觸發(fā)緩存失效操作;
也即清除緩存,被作用的方法每次調(diào)用都會(huì)執(zhí)行方法體,并且使用方法體返回更新緩存;
4. @Caching;
@Caching作用在方法上,注解中混合使用@Cacheable、@CachePut、@CacheEvict操作,完成復(fù)雜、多種緩存操作;
比如同一個(gè)方法,關(guān)聯(lián)多個(gè)緩存,并且其緩存名字、緩存key都不同時(shí),或同一個(gè)方法有增刪改查的緩存操作等,被作用的方法每次調(diào)用都會(huì)執(zhí)行方法體。
5. @CacheConfig;
在類上設(shè)置當(dāng)前緩存的一些公共設(shè)置,也即類級(jí)別的全局緩存配置。
4. 使用Spring Cache的緩存注解操作Redis;
整體步驟:
1). 修改項(xiàng)目入口類;
2). 編寫演示用實(shí)體類;
3). 編寫演示用Service類;
4). 編寫演示用Controller類;
1). 修改項(xiàng)目入口類;
在Spring Boot項(xiàng)目入口類App.java上添加注解:@EnableCaching,App.java類整體如下:
package com.github.dylanz666;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @author : dylanz
* @since : 10/28/2020
*/
@SpringBootApplication
@EnableCaching
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
2). 編寫演示用實(shí)體類;
在domain包內(nèi)建立演示用實(shí)體類User.java,簡(jiǎn)單擼點(diǎn)代碼演示一下:
package com.github.dylanz666.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author : dylanz
* @since : 10/31/2020
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String userName;
private String roleName;
@Override
public String toString() {
return "{" +
"\"userName\":\"" + userName + "\"," +
"\"roleName\":" + roleName + "" +
"}";
}
}
3). 編寫演示用Service類;
重點(diǎn)來(lái)了,在service包內(nèi)新建UserService類,編寫帶有Spring Cache注解的方法們,代碼如下:
package com.github.dylanz666.service;
import com.github.dylanz666.domain.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
/**
* @author : dylanz
* @since : 10/31/2020
*/
@Service
public class UserService {
@Cacheable(value = "content", key = "'id_'+#userName")
public User getUserByName(String userName) {
System.out.println("io operation : getUserByName()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName("dylanz");
userInDb.setRoleName("adminUser");
return userInDb;
}
@CachePut(value = "content", key = "'id_'+#user.userName")
public User updateUser(User user) {
System.out.println("io operation : updateUser()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName(user.getUserName());
userInDb.setRoleName(user.getRoleName());
return userInDb;
}
@CacheEvict(value = "content", key = "'id_'+#user.userName")
public void deleteUser(User user) {
System.out.println("io operation : deleteUser()");
}
@Caching(cacheable = {
@Cacheable(cacheNames = "test", key = "'id_'+#userName")
}, put = {
@CachePut(value = "testGroup", key = "'id_'+#userName"),
@CachePut(value = "user", key = "#userName")
})
public User getUserByNameAndDoMultiCaching(String userName) {
System.out.println("io operation : getUserByNameAndDoMultiCaching()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName("dylanz");
userInDb.setRoleName("adminUser");
return userInDb;
}
}
簡(jiǎn)單介紹一下:
- 在UserService的方法內(nèi)模擬了一下數(shù)據(jù)庫(kù)中的數(shù)據(jù),用于模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù),代碼為:
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName("dylanz");
userInDb.setRoleName("adminUser");
我寫了4個(gè)方法getUserByName(String userName)、
updateUser(User user)、deleteUser(User user)、getUserByNameAndDoMultiCaching(String userName)分別用于演示查詢緩存、更新緩存、刪除緩存,以及同一個(gè)方法使用混合緩存注解;緩存注解中使用了2個(gè)基本屬性(還有其他屬性),value(也可以用cacheNames)和key,key如果是從方法體獲取的,則需要在key前加#號(hào),比如key="#userName"或者key = "'id_'+#userName",當(dāng)然也可以寫死如key="test123";
緩存注解中的key屬性值可以從對(duì)象中獲取,如:key = "'id_'+#user.userName";
緩存注解的value(或cacheNames)屬性、key屬性共同構(gòu)成了Redis服務(wù)端中完整的key,如value = "content", key = "test123",則在Redis服務(wù)端,其完整key為:content::test123;
@Caching中可混合使用緩存注解@Cacheable、@CachePut、@CacheEvict,對(duì)應(yīng)的屬性是:cacheable、put、evict,復(fù)雜場(chǎng)景有幫助!
3). 編寫演示用Controller類;
在controller包內(nèi)新建UserController.java類,編寫4個(gè)API,分別使用Service中的4個(gè)帶有Spring Cache注解的方法們,代碼如下:
package com.github.dylanz666.controller;
import com.github.dylanz666.domain.User;
import com.github.dylanz666.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author : dylanz
* @since : 10/31/2020
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("")
public @ResponseBody User getUserByName(@RequestParam String userName) {
return userService.getUserByName(userName);
}
@PutMapping("")
public @ResponseBody
User updateUser(@RequestBody User user) {
return userService.updateUser(user);
}
@DeleteMapping("")
public @ResponseBody String deleteUser(@RequestBody User user) {
userService.deleteUser(user);
return "success";
}
@GetMapping("/multiCaching")
public @ResponseBody User getUserByNameAndDoMultiCaching(@RequestParam String userName) {
return userService.getUserByNameAndDoMultiCaching(userName);
}
}
至此,項(xiàng)目整體結(jié)構(gòu):
5. Spring Cache + Redis 緩存演示;
1). 啟動(dòng)Redis服務(wù)端;
redis-server.exe redis.windows.conf
2). 啟動(dòng)Redis客戶端,進(jìn)入monitor模式;
啟動(dòng):
redis-cli.exe -h 127.0.0.1 - p 6379
auth Redis!123
在Redis客戶端,進(jìn)入monitor模式,命令:
monitor
3). 啟動(dòng)項(xiàng)目;
4). postman訪問(wèn)Controller中的API;
- 首先在設(shè)置緩存前,直接調(diào)用獲取user的API(GET方法):http://127.0.0.1:8080/api/user?userName=dylanz
我們會(huì)看到,第一次調(diào)用API時(shí),執(zhí)行了方法體,并完成了第一次Redis緩存!
- 調(diào)用更新user的API(PUT方法):http://127.0.0.1:8080/api/user
- 更新后,再次調(diào)用獲取user的API:
我們會(huì)看到,緩存確實(shí)被更新了!
- 調(diào)用刪除user的API(DELETE方法):http://127.0.0.1:8080/api/user
- 緩存刪除后,再次調(diào)用獲取user的API:
我們會(huì)看到,緩存被刪除后,再次獲取緩存則會(huì)自動(dòng)再次執(zhí)行方法體,并完成新一輪的緩存!
- 調(diào)用擁有混合緩存注解的API(GET方法):http://127.0.0.1:8080/api/user/multiCaching?userName=dylanz
同時(shí)為了更直觀的讓大家看到緩存有起到效果,我在Service層的方法內(nèi),都往控制臺(tái)打印了一些文字,如:io operation : getUserByName()...
我分別各調(diào)用了3次具有更新緩存的API、刪除緩存的API、獲取緩存的API、混合使用緩存注解的API,控制臺(tái)打印:
我們會(huì)發(fā)現(xiàn):更新緩存、刪除緩存、混合使用緩存注解的緩存,每次都有執(zhí)行方法體,而獲取緩存只執(zhí)行了一次方法體,這跟我們之前介紹緩存注解的時(shí)候是一致的,再次驗(yàn)證了之前對(duì)緩存注解的認(rèn)識(shí)!
6. 配置類方式配置和管理Redis緩存;
在config包內(nèi)創(chuàng)建Redis配置類RedisConfig.java,并編寫如下代碼:
package com.github.dylanz666.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
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.*;
import java.time.Duration;
/**
* @author : dylanz
* @since : 10/31/2020
*/
@Configuration
@EnableCaching
public class RedisConfig {
private final Duration defaultTtl = Duration.ofMinutes(1);
/**
* 配置自定義redisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
/**
* 配置緩存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(defaultTtl)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
}
稍微解讀一下:
1). 默認(rèn)情況下Redis的序列化會(huì)使用jdk序列化器JdkSerializationRedisSerializer,而該序列化方式,在存儲(chǔ)內(nèi)容時(shí)除了屬性的內(nèi)容外還存了其它內(nèi)容在里面,總長(zhǎng)度長(zhǎng),且不容易閱讀,因此我們自定義了序列化策略:redisTemplate(),使用該自定義序列化方式后,存儲(chǔ)在Redis的內(nèi)容就比較容易閱讀了!
2). Spring Cache注解方式,默認(rèn)是沒(méi)有過(guò)期概念的,即設(shè)置了緩存,則一直有效,這顯然不能完全滿足使用要求,而RedisConfig中的CacheManager給我們提供了一種方式來(lái)設(shè)置緩存過(guò)期策略!
3). cacheManager()用于配置緩存管理器,我們?cè)诜椒▋?nèi)配置了一個(gè)entryTtl為1分鐘,TTL即Time To Live,代表緩存可以生存多久,我沒(méi)有在cacheManager()方法內(nèi)指明哪個(gè)緩存,因此該TTL是針對(duì)所有緩存的,是全局性的,也是我們?cè)O(shè)置的默認(rèn)TTL;
4). 我們可以給不同的key設(shè)置不同的TTL,但hard code的緩存key就有點(diǎn)多啦,此處不介紹啦!
5). 該配置類使用了注解@EnableCaching,則Spring Boot入口類App.java中的@EnableCaching注解就可以刪除啦!
7. 動(dòng)態(tài)緩存有效期的實(shí)現(xiàn);
動(dòng)態(tài)緩存有效期的實(shí)現(xiàn)有多種方式,如:
1). 在緩存注解上使用不同的自定義的CacheManager;
2). 自定義Redis緩存有效期注解;
3). 利用在Spring Cache緩存注解中value屬性中設(shè)置的值,設(shè)置緩存有效期;
1). 在緩存注解上使用不同的自定義的CacheManager;
在第6步中,我們使用了一個(gè)全局的CacheManager,其實(shí)這個(gè)可以更靈活,可以用于給不同的緩存設(shè)置不同的TTL,也即緩存過(guò)期。
我們可以在RedisConfig.java中編寫多個(gè)不同名字的CacheManager,每個(gè)CacheManager使用不同的TTL。
然后緩存注解中使用不同的CacheManager,就能達(dá)到不同緩存有不同TTL的目的啦,并且沒(méi)有在CacheManager中hard code緩存key,演示代碼:
-
RedisConfig.java演示代碼:
package com.github.dylanz666.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.*;
import java.time.Duration;
/**
* @author : dylanz
* @since : 10/31/2020
*/
@Configuration
@EnableCaching
public class RedisConfig {
private final Duration defaultTtl = Duration.ofMinutes(1);
private final Duration ttl10s = Duration.ofSeconds(10);
/**
* 配置自定義redisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
/**
* 配置緩存管理器
*/
@Bean
@Primary
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(defaultTtl)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
@Bean
public CacheManager cacheManager10s(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(ttl10s)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(redisCacheConfiguration).build();
}
}
注意:當(dāng)有多個(gè)CacheManager時(shí),要設(shè)置一個(gè)默認(rèn)的CacheManager,即在默認(rèn)的CacheManager上添加@Primary注解,否則啟動(dòng)項(xiàng)目會(huì)報(bào)錯(cuò)!
-
在想演示的緩存注解上添加cacheManger屬性,并指定RedisConfig.java中的CacheManager,如:
@CachePut(value = "content", key = "'id_'+#user.userName", cacheManager = "cacheManager10s")
public User updateUser(User user) {
System.out.println("io operation : updateUser()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName(user.getUserName());
userInDb.setRoleName(user.getRoleName());
return userInDb;
}
那么,使用了cacheManager10s的緩存,就會(huì)按我們的設(shè)置,只緩存10秒鐘,其他沒(méi)設(shè)置cacheManager的緩存,則會(huì)緩存1分鐘,親測(cè)有效!
自定義cacheManager的方式提供了一定的靈活性,但當(dāng)緩存有效期越來(lái)越多樣時(shí),RedisConfig.java中的代碼就會(huì)越來(lái)越多,越來(lái)越難以管理。接下來(lái)介紹另外一種更為靈活的方式!
2). 自定義Redis緩存有效期注解;
這種解題思路是:自定義Redis緩存有效期注解,通過(guò)切面編程,在切面中完成Redis緩存有效期的設(shè)置。
關(guān)于切面編程,可以參考之前文章:Spring Boot之面向切面編程:Spring AOP;
(1). 編寫時(shí)間單位枚舉類;
出于靈活性考慮,我將在自定義的注解中使用2個(gè)屬性,一個(gè)為duration(時(shí)間大小)屬性,一個(gè)為unit(時(shí)間單位)屬性,duration與unit結(jié)合使用,可以靈活的設(shè)置緩存的有效期!
項(xiàng)目?jī)?nèi)新建constant包,在包內(nèi)新建時(shí)間單位枚舉類,編寫代碼如下:
package com.github.dylanz666.constant;
/**
* @author : dylanz
* @since : 11/02/2020
*/
public enum DurationUnitEnum {
NANO,
MILLI,
SECOND,
MINUTE,
HOUR,
DAY
}
(2). 自定義注解;
項(xiàng)目?jī)?nèi)新建annotation包,在包內(nèi)新建注解類Ttl.java(名字隨意),編寫注解類:
package com.github.dylanz666.annotation;
import com.github.dylanz666.constant.DurationUnitEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author : dylanz
* @since : 11/01/2020
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Ttl {
//設(shè)置緩存有效時(shí)間,單位為秒,默認(rèn)60秒
long duration() default 60;
//時(shí)間單位,默認(rèn)為秒,SECOND,可選的有:NANO,MILLI,SECOND,MINUTE,HOUR,DAY
DurationUnitEnum unit() default DurationUnitEnum.SECOND;
}
(3). 創(chuàng)建切面編程類,并完成自定義注解的邏輯實(shí)現(xiàn);
經(jīng)過(guò)實(shí)驗(yàn),如果沒(méi)有在切面中對(duì)Spring Cache的緩存注解、自定義的動(dòng)態(tài)緩存有效期注解進(jìn)行排序,則可能出現(xiàn)自定義的注解先執(zhí)行,Spring Cache的緩存注解后執(zhí)行,導(dǎo)致緩存有效期被默認(rèn)的TTL覆蓋,即:自定義的注解沒(méi)起到作用!
因此,需要分別對(duì)Spring Cache的緩存注解、自定義的動(dòng)態(tài)緩存有效期注解編寫切面類,并使用@Order進(jìn)行切面排序!
在config包內(nèi)新建2個(gè)切面類:AOPConfig.java和
AOPConfig2.java。
AOPConfig.java代碼:
package com.github.dylanz666.config;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* @author : dylanz
* @since : 11/02/2020
*/
@Configuration
@Aspect
@Order(1)
public class AOPConfig {
@Around("@annotation(org.springframework.cache.annotation.Cacheable)")
public Object cacheableAop(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
@Around("@annotation(org.springframework.cache.annotation.CachePut)")
public Object cachePutAop(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
@Around("@annotation(org.springframework.cache.annotation.CacheEvict)")
public Object cacheEvictAop(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
@Around("@annotation(org.springframework.cache.annotation.Caching)")
public Object cachingAop(final ProceedingJoinPoint joinPoint) throws Throwable {
return joinPoint.proceed();
}
}
AOPConfig2.java代碼:
package com.github.dylanz666.config;
import com.github.dylanz666.annotation.Ttl;
import com.github.dylanz666.constant.DurationUnitEnum;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.Duration;
/**
* @author : dylanz
* @since : 11/01/2020
*/
@Configuration
@Aspect
@Order(2)
public class AOPConfig2 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@After("@annotation(com.github.dylanz666.annotation.Ttl)")
public void simpleAop2(final JoinPoint joinPoint) throws Throwable {
//獲取一些基礎(chǔ)信息
String methodName = joinPoint.getSignature().getName();
Class<?> targetClass = joinPoint.getTarget().getClass();
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
Method objMethod = targetClass.getMethod(methodName, parameterTypes);
//獲取想要設(shè)置的緩存有效時(shí)長(zhǎng)
Ttl cacheDuration = objMethod.getDeclaredAnnotation(Ttl.class);
long duration = cacheDuration.duration();
DurationUnitEnum unit = cacheDuration.unit();
//獲取緩存注解的values/cacheNames和key
String[] values = getValues(objMethod);
String key = getKey(objMethod);
//準(zhǔn)備好變量名與變量值的上下文綁定
Parameter[] parameters = objMethod.getParameters();
Object[] args = joinPoint.getArgs();
StandardEvaluationContext ctx = new StandardEvaluationContext();
for (int i = 0; i < parameters.length; i++) {
ctx.setVariable(parameters[i].getName(), args[i]);
}
//獲取真實(shí)的key,而不是變量形式的key
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression(key);
String realKey = expression.getValue(ctx, String.class);
//設(shè)置緩存生效時(shí)長(zhǎng),即過(guò)期時(shí)間
setExpiration(values, realKey, duration, unit);
}
private void setExpiration(String[] values, String realKey, long duration, DurationUnitEnum unit) throws InterruptedException {
Duration expiration;
if (unit == DurationUnitEnum.NANO) {
expiration = Duration.ofNanos(duration);
} else if (unit == DurationUnitEnum.MILLI) {
expiration = Duration.ofMillis(duration);
} else if (unit == DurationUnitEnum.MINUTE) {
expiration = Duration.ofMinutes(duration);
} else if (unit == DurationUnitEnum.HOUR) {
expiration = Duration.ofHours(duration);
} else if (unit == DurationUnitEnum.DAY) {
expiration = Duration.ofDays(duration);
} else {
expiration = Duration.ofSeconds(duration);
}
for (String value : values) {
String redisKey = value + "::" + realKey;
stringRedisTemplate.expire(redisKey, expiration);
}
}
private String[] getValues(Method objMethod) {
Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class);
CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class);
CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class);
//value
if (cacheable != null && cacheable.value().length > 0) {
return cacheable.value();
}
if (cachePut != null && cachePut.value().length > 0) {
return cachePut.value();
}
if (cacheEvict != null && cacheEvict.value().length > 0) {
return cacheEvict.value();
}
//cacheNames
if (cacheable != null && cacheable.cacheNames().length > 0) {
return cacheable.cacheNames();
}
if (cachePut != null && cachePut.cacheNames().length > 0) {
return cachePut.cacheNames();
}
if (cacheEvict != null && cacheEvict.cacheNames().length > 0) {
return cacheEvict.cacheNames();
}
return new String[]{};
}
private String getKey(Method objMethod) {
Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class);
CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class);
CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class);
if (cacheable != null && !cacheable.key().equals("")) {
return cacheable.key();
}
if (cachePut != null && !cachePut.key().equals("")) {
return cachePut.key();
}
if (cacheEvict != null && !cacheEvict.key().equals("")) {
return cacheEvict.key();
}
return "";
}
}
由于@Caching比較復(fù)雜,我們沒(méi)有將Ttl.java應(yīng)用到@Caching注解哦,讀者可自行探索!
(3). 使用自定義的緩存有效期注解;
只需要在有使用@Cacheable、@CachePut、@CacheEvict注解的方法上再添加使用@Ttl注解即可,如:
@Cacheable(value = "content", key = "'id_'+#userName")
@Ttl(duration = 10, unit = DurationUnitEnum.SECOND)
public User getUserByName(String userName) {
System.out.println("io operation : getUserByName()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName("dylanz");
userInDb.setRoleName("adminUser");
return userInDb;
}
和:
@CachePut(value = "content", key = "'id_'+#user.userName")
@Ttl(duration = 1, unit = DurationUnitEnum.HOUR)
public User updateUser(User user) {
System.out.println("io operation : updateUser()");
//模擬從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)
User userInDb = new User();
userInDb.setUserName(user.getUserName());
userInDb.setRoleName(user.getRoleName());
return userInDb;
}
注:如果@Ttl注解內(nèi)沒(méi)有duration,則默認(rèn)為60,如果unit單位沒(méi)有,則默認(rèn)為秒,如:
- @Ttl:代表緩存60秒;
- @Ttl(duration = 10):代表緩存10秒;
- @Ttl(duration = 10,DurationUnitEnum.MINUTE):代表緩存10分鐘;
大家可以根據(jù)自己需要再進(jìn)行調(diào)整。
至此項(xiàng)目整體結(jié)構(gòu):
演示一下:
同時(shí)我們也能在API的返回中,看到緩存失效的效果!
3). 利用在Spring Cache緩存注解中value屬性中設(shè)置的值,設(shè)置緩存有效期;
這種方式同樣可以利用切面編程,在獲取到value屬性后,截取我們想設(shè)置的緩存有效期時(shí)長(zhǎng),然后設(shè)置緩存有效期。
例如,我們有一個(gè)Spring Cache緩存注解:
@CacheEvict(value = "content#3600", key = "'id_'+#user.userName")
參照自定義注解的代碼,我們可以很輕松獲取到value為 "content#3600",然后可以截取3600為緩存有效期時(shí)長(zhǎng),而單位可以默認(rèn)設(shè)置為秒(根據(jù)實(shí)際要求設(shè)置),最后還是一樣去設(shè)置redis key的expiration。整體與自定義注解非常像,我們就不做過(guò)多介紹啦!
也有使用自定義的RedisCacheManager來(lái)實(shí)現(xiàn)的,未來(lái)有機(jī)會(huì)再深入哈!
上述3種方式都可以完成動(dòng)態(tài)緩存有效期的設(shè)置,而我最喜歡的莫過(guò)于:自定義注解的方式,該方式靈活、易于理解,用戶編碼時(shí)也非常簡(jiǎn)單!
至此Spring Cache + Redis完成靈活的Redis操作到此告一段落啦,Spring Boot項(xiàng)目中使用Redis緩存再上層樓,有沒(méi)有Get到?
如果本文對(duì)您有幫助,麻煩點(diǎn)贊、關(guān)注!
謝謝!