在實現上述功能之前先來點基礎的,redis在SpringBoot項目中常規的用法,好對緩存和redis客戶端的使用有一定了解。
1.添加依賴 redis客戶端依賴(連接redis服務端必備 )
<!-- 客戶端依賴二選一 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<!-- redis所需依賴 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<!-- mysql依賴-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybaties 依賴-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
2. yaml配置項
server:
port: 8702
spring:
application:
name: eurekaClient8702 #此處切記不能用a_b命名
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test
username: root
password: admin
redis:
host: 192.168.32.130
port: 6379
timeout: 20000
3. 配置類
/**
* @author Heian
* @time 19/07/07 16:59
* @description:配置類
*/
@Configuration
@EnableCaching//開啓緩存註解
//@Profile ("single")
public class webconfig {
// 配置Spring Cache註解功能:指定緩存類型redis
@Bean
public CacheManager redisCacheManage(RedisConnectionFactory redisConnectionFactory) {
System.out.println ("-----------------Spring 定義CacheManager的緩存類型-----------------");
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
return cacheManager;
}
}
下面進行一些簡單的案例測試:
測試一:往redis存值(當然,redis服務必須是開啓的)
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomersApplicationTests {
// 直接注入StringRedisTemplate,則代表每一個操作參數都是字符串
@Autowired
private StringRedisTemplate stringRedisTemplate;
//測試一:存值
@Test
public void setByCache() {
stringRedisTemplate.opsForValue().set("k1", "我是k1");//如果是中文stringRedisTemplate會默認採用String類的序列化機制
}
}
測試二:對象緩存功能(先查看緩存中有無該對象,有則直接讀取;無則加載到mysql數據庫並添加到到redis緩存中)
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomersApplicationTests {
// 參數可以是任何對象,默認由JDK序列化
@Resource
private RedisTemplate<Integer,User> redisTemplate;
@Autowired
private UserService userService;
@Test//對象緩存功能
public void findUser() {
User user = null;
int id = 1;
// 1、 判定緩存中是否存在
user = (User) redisTemplate.opsForValue().get(id);
if (user != null) {
System.out.println("從緩存中讀取到值:" + user.toString ());
}else {
// 2、不存在則讀取數據庫
user = userService.getUserFromDB (id);
// 3、 同步存儲value到緩存。
redisTemplate.opsForValue().set(id, user);
System.out.println (user.toString ());
}
}
}
/**
* @author Heian
* @time 19/07/07 17:24
* @description:
*/
@Service
public class UserService {
@Autowired
private UserDao userDao;
//從數據庫獲取代當前User對象
public User getUserFromDB(int id) {
User user = userDao.getUserById (id);
return user;
}
/* @Cacheable 支持如下幾個參數:
* value:緩存位置名稱,不能爲空,如果使用EHCache,就是ehcache.xml中聲明的cache的name
* key:緩存的key,默認爲空,既表示使用方法的參數類型及參數值作爲key,支持Sp EL表達式
* condition:觸發條件,只有滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持Sp EL表達式
*/
// value~單獨的緩存前綴
// key緩存key 可以用springEL表達式 cache-1:123
@Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
public User findUserById(int id) {
// 讀取數據庫
User user = new User(id, "Heian",26);
System.out.println("從數據庫中讀取到數據:" + user);
return user;
}
@CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
public void deleteUserById(int id) {
// 先數據庫刪除,成功後,刪除Cache
// 先判斷Cache裏面是不是有?有則刪除
System.out.println("用戶從數據庫刪除成功,請檢查緩存是否清除~~" + id);
}
// 如果數據庫更新成功,更新redis緩存
@CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
public User updateUser(User user){
// 先更新數據庫,更成功
// 更新緩存
// 讀取數據庫
System.out.println("數據庫進行了更新,檢查緩存是否一致");
return user; // 返回最新內容,代表更新成功
}
}
備註:此時數據庫是有id=1的數據的,但緩存中也沒有,ok,執行,第一次執行發現緩存中沒有此對象,然後查詢mysql數據庫將返回的對象添加在緩存中。第二次執行會直接從數據庫中查的,如下圖。
測試三:利用spring的註解實現緩存功能
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomersApplicationTests {
@Autowired
private UserService userService;
@Test
public void testSpringCache(){
User user = userService.findUserById (1);
System.out.println("測試類:" +user.toString ());
}
}
/**
* @author Heian
* @time 19/07/07 17:24
* @description:
*/
@Service
public class UserService {
@Autowired
private UserDao userDao;
/* @Cacheable 支持如下幾個參數:
* value:緩存位置名稱,不能爲空,如果使用EHCache,就是ehcache.xml中聲明的cache的name
* key:緩存的key,默認爲空,既表示使用方法的參數類型及參數值作爲key,支持Sp EL表達式
* condition:觸發條件,只有滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持Sp EL表達式
*/
// value~單獨的緩存前綴
// key緩存key 可以用springEL表達式 cache-1:123
@Cacheable(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
public User findUserById(int id) {
// 讀取數據庫
User user = userDao.getUserById (id);
System.out.println("從數據庫中讀取到數據:" + user);
return user;
}
@CacheEvict(cacheManager = "redisCacheManage", value = "cache-1", key = "#id")
public void deleteUserById(int id) {
// 先數據庫刪除,成功後,刪除Cache
// 先判斷Cache裏面是不是有?有則刪除
System.out.println("用戶從數據庫刪除成功,請檢查緩存是否清除~~" + id);
}
// 如果數據庫更新成功,更新redis緩存
@CachePut(cacheManager = "redisCacheManage", value = "cache-1", key = "#user.id", condition = "#result ne null")
public User updateUser(User user){
// 先更新數據庫,更成功
// 更新緩存
// 讀取數據庫
System.out.println("數據庫進行了更新,檢查緩存是否一致");
return user; // 返回最新內容,代表更新成功
}
}
備註:點擊執行時,首先會執行service中的查詢的代碼,然後spring註解@Cacheable會默默的往redis存入緩存,當你執行第二遍時,不在執行service裏的邏輯代碼,而是直接從緩存中拿到值了。
那麼正題來了,如果我們不使用Spring的註解,或者說想使用的更加靈活的使用,又該如何使用呢?肯定又要用到Aop了。現在就對Aop進行一個簡單的瞭解,並且使用Aop自定義一個註解,完成和@Cacheable同樣的功能。
Aop概念
AOP是Spring提供的兩個核心功能之一:IOC(控制反轉),AOP(Aspect Oriented Programming 面向切面編程);IOC有助於應用對象之間的解耦,AOP可以實現橫切關注點和它所影響的對象之間的解耦,它通過對既有的程序定義一個橫向切入點,然後在其前後切入不同的執行內容,來拓展應用程序的功能,常見的用法如:打開事務和關閉事物,記錄日誌,統計接口時間等。AOP不會破壞原有的程序邏輯,拓展出的功能和原有程序是完全解耦的,因此,它可以很好的對業務邏輯的各個部分進行隔離,從而使業務邏輯的各個部分之間的耦合度大大降低,提高了部分程序的複用性和靈活性。
實現aop切面,主要有以下幾個關鍵點需要了解:
- @Aspect,此註解將一個類定義爲一個切面類;
- @Pointcut,此註解可以定義一個切入點,可以是規則表達式,也可以是某個package下的所有函數,也可以是一個註解等,其實就是執行條件,滿足此條件的就切入;
- 然後可以定義切入位置,我們可以選擇在切入點的不同位置進行切入:
- @Before在切入點開始處切入內容;
- @After在切入點結尾處切入內容;
- @AfterReturning在切入點return內容之後切入內容(可以用來對返回值做一些處理);
- @Around在切入點前後切入內容,並自己控制何時執行切入點自身的內容;
- @AfterThrowing用來處理當切入內容部分拋出異常之後的處理邏輯;
自定義註解進行一個實現來替代Spring的CacheManage註解
第一:首先自定義註解類,運行於方法之上
package com.example.customers.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Heian
* @time 19/07/12 8:21
* @description:自定義緩存組件
*/
@Target (ElementType.METHOD)//作用於方法之上
@Retention (RetentionPolicy.RUNTIME)//運行期間生效
public @interface MyRedisCache {
//key值:存在redis的key,可以使用springEL表達式,可以使用方法執行的一些參數
String key();
}
第二:設置切面類,這裏可以使用環繞@Around(等價於@Before+@After),邏輯和上述類似,先定義切點,指出切入的對象,然後再你要切入的邏輯,我這裏的邏輯和上述類似:
- 通過joinpoint拿到簽名,然後取得對應註解上的方法和註解標記的參數
- 根據SpringEl提供的類來解析注入值,SpringEL好處可參考博文:https://blog.csdn.net/u011305680/article/details/80271423
- 先從緩存中拿值,拿不到從數據庫拿,並存於redis,以便於下次取值可以直接去緩存中拿。
package com.example.customers.aspect;
import com.example.customers.annotations.MyRedisCache;
import com.example.customers.entity.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
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 org.springframework.stereotype.Component;
import java.io.Reader;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author Heian
* @time 19/07/12 8:30
* @description:自定義註解的切面
*/
@Component
@Aspect
public class MyRedisCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
//返回類型任意,方法參數任意,方法名任意
@Pointcut(value = "@annotation(com.example.customers.annotations.MyRedisCache)")
public void myredisPointcut(){
}
@Around ("myredisPointcut()")
public Object myredisAround(ProceedingJoinPoint joinPoint){
Object obj = null;
try {
MethodSignature signature = (MethodSignature)joinPoint.getSignature ();
//取得使用註解的方法 有了方法就能:方法的參數、返回值類型、註解、該方法的所在類等等
Method method =signature.getMethod ();
// 通過反射拿到該方法的註解類的比如PostMapping的方法 參數必須是註解
MyRedisCache myRedisCache = method.getAnnotation (MyRedisCache.class);
String key = myRedisCache.key ();//#{userid}
EvaluationContext context = new StandardEvaluationContext ();
// joinPoint 取該調用註解方法的傳來的具體參數的值如:1
Object[] args = joinPoint.getArgs();
DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
// 取該調用註解方法的參數如:id
String[] parameterNames = discover.getParameterNames(method);
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i].toString());// id :1
}
//拿到key值後 然後進行解析
ExpressionParser parser = new SpelExpressionParser ();
Expression expression = parser.parseExpression (key);//"#id" 取得自定義註解的key的內容
String realKey = expression.getValue(context).toString();//映射#id" --> id
// 1、 判定緩存中是否存在
obj = redisTemplate.opsForValue ().get (realKey);
if (obj != null) {
System.out.println("從緩存中讀取到值:" + obj);
return obj;
}
// 2、不存在則執行方法,相當於我們Method.invoke(對象,"setName()");
obj = joinPoint.proceed();
//3、並且存於redis中
redisTemplate.opsForValue ().set (realKey,obj);
} catch (Throwable e) {
e.printStackTrace ();
}
return obj;
}
}
第三:定義Controller層類和Service類進行測試
package com.example.customers.controller;
import com.example.customers.annotations.MyRedisCache;
import com.example.customers.entity.User;
import com.example.customers.service.AopService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author Heian
* @time 19/07/11 14:07
* @description:學習Aop測試
*/
@RestController
@RequestMapping("/StudyAop")
public class AopController {
private Logger logger = LoggerFactory.getLogger(AopController.class);
@Autowired
private AopService aopService;
@GetMapping("test1")
public String test1(){
logger.info ("進入test1()方法");
return "歡迎進入Aop的學習1";
}
@GetMapping("test2")
public String test2() throws InterruptedException{
TimeUnit.SECONDS.sleep (5);
return "歡迎進入Aop的學習2";
}
@PostMapping("test3")
public User test3(@RequestParam int id){
User user = aopService.implRedisCache (id);
logger.info ("返回的對象" + user.toString ());
return user;
}
}
package com.example.customers.service;
import com.example.customers.annotations.MyRedisCache;
import com.example.customers.dao.UserDao;
import com.example.customers.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author Heian
* @time 19/07/12 11:29
* @description:
*/
@Service
public class AopService {
@Autowired
private UserDao userDao;
@MyRedisCache(key = "#id")
public User implRedisCache(int id){
User user = userDao.getUserById (id);
System.out.println ("從數據庫中讀的對象爲"+user.toString ());
return user;
}
}
備註:這裏使用Postman測試(我已在本地redis服務清空了數據flushdb,所以第一次訪問是沒有緩存的)
第一次訪問:
第二次訪問:(接口耗時也減少了)
ok,至此完成,這就利用AOP實現了兩個功能:1.統計接口耗時和訪問次數 2.自定義緩存組件。
最近單徐循環的歌曲:水木年華的一首《中學時代》