隨筆(八) 自定義redis快取註解(基於springboot)
阿新 • • 發佈:2018-12-20
前言:
最近專案開發中需要使用redis快取為資料庫降壓。由於在構建系統時沒有使用快取,後期加入快取的時候不想對業務程式碼上新增,造成程式碼入侵,所有封裝了一套自定義快取類,處理快取。
開發環境:
win10+IntelliJ IDEA +JDK1.8
springboot版本:springboot 2.0.4 ——2.0後的springboot增加了挺多新特性,暫時先不做了解
專案結構:名字起的不太好,因為隨便起的了。
注:
Authoriztions類主要是AOP攔截,以及redis處理的(懶得換地方,就寫在這了,用的話可以分開來)
MyTest是主要寫被攔截的的方法。
RedisCacheAble介面是進行快取的註解
RedisCacheEvict是刪除快取的註解
Users 一個物件。用於簡單測試。
Pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wen</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
上程式碼:
首先準備一個Controller,用來處理做前端展示。
package com.wen.demo.controller; import com.wen.demo.utils.MyTest; import com.wen.demo.utils.Users; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.lang.reflect.InvocationTargetException; /** * @Description: * @Author: Gentle * @date 2018/10/26 20:31 */ @RestController public class HelloController { @Autowired MyTest myTest; @RequestMapping(value = "hello") public Users test() throws IllegalAccessException, InstantiationException, InvocationTargetException { Users users = new Users(); users.setId(20); users.setWen("wen"); return myTest.test(users); } @RequestMapping(value = "delete") public int delete() throws IllegalAccessException, InstantiationException, InvocationTargetException { return myTest.abc(20); } }
接下來,我們寫一個自定義兩個註解:
使用快取的註解
package com.wen.demo.utils;
import java.lang.annotation.*;
/**
* @Description:
* @Author: Gentle
* @date 2018/10/14 17:04
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisCacheAble {
//欄位名,用於存雜湊欄位,該欄位要isHash為true的時候才能用
String field() default "field" ;
//快取的名字,配合一下的key一起使用
String cacheName() default "cacheName" ;
//key,傳入的物件,例如寫的是#id id=1 鍵一定要寫#
//生成的redis鍵為 cacheName:1
String key() ;
//判斷是否使用雜湊型別
boolean isHash() default false;
//設定鍵的存活時間。預設-1位永久。時間是按秒算
int time() default -1;
}
刪除快取的註解:
package com.wen.demo.utils;
import java.lang.annotation.*;
/**
* @Description:
* @Author: Gentle
* @date 2018/10/14 17:04
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisCacheEvict {
//欄位名,用於存雜湊欄位,該欄位要isHash()為true的時候才能用
String field() default "field" ;
//快取的名字,配合一下的key一起使用
String cacheName() default "cacheName" ;
//key,傳入的物件,例如寫的是#id id=1 鍵一定要寫#
//生成的redis鍵為 cacheName:1
String key() ;
//判斷是否使用雜湊型別
boolean isHash() default false;
}
用於測試的Users物件
package com.wen.demo.utils;
import lombok.Data;
/**
* @Description:
* @Author: Gentle
* @date 2018/10/26 20:32
*/
@Data
public class Users {
private Integer id;
private String wen;
}
用於測試註解的類,被攔截的類:
package com.wen.demo.utils;
import org.springframework.stereotype.Component;
/**
* @Description:
* @Author: Gentle
* @date 2018/10/14 17:23
*/
@Component
public class MyTest {
@RedisCacheAble(key="#users.id",cacheName = "wen")
public Users test(Users users){
users.setWen("wen");
return users;
}
@RedisCacheAble(key="#id",cacheName = "wen")
public int test(int id){
return 100;
}
@RedisCacheEvict(key = "#id",cacheName = "wen")
public int abc(int id){
return 100;
}
}
注意:@RedisCacheAble(key="#users.id",cacheName = "wen")
方法Test(Users users) ,標紅色部分屬性或物件名必須一致,否則找不到相關屬性。方法中的引數,需要和鍵中寫的一致且鍵一定要加#
核心類:
package com.wen.demo.utils;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisShardInfo;
import java.lang.reflect.Method;
/**
* @Description:
* @Author: Gentle
* @date 2018/10/14 17:07
*/
@Component
@Aspect
public class Authorizations {
/**
* 這個懶得寫成一個類了。。就湊合這樣寫了。整合到自己的專案,可以刪除,修改
*/
Jedis jedis = new Jedis(setJedisShardInfo());
public JedisShardInfo setJedisShardInfo(){
JedisShardInfo jedisShardInfo = new JedisShardInfo("自己redis的Ip地址");
jedisShardInfo.setPassword("redis密碼");
return jedisShardInfo;
}
/**
* 正文開始是如下
*/
private static final String Default_String = ":";
@Around("@annotation(redisCacheAble)")
public Object handlers(ProceedingJoinPoint joinPoint, RedisCacheAble redisCacheAble) {
try {
//拿到存入redis的鍵
String handler = returnRedisKey(joinPoint, redisCacheAble.key(), redisCacheAble.cacheName());
//查詢redis,看有沒有。有就直接返回。沒有。就GG
String redisCacheValue = getRedisCacheValue(redisCacheAble, handler);
if (redisCacheValue != null) {
System.out.println("使用快取" + redisCacheValue);
//拿到返回值型別
Class<?> methodReturnType = getMethodReturnType(joinPoint);
//處理從redis拿出的字串。
Object o= JSON.parseObject(redisCacheValue,methodReturnType);
System.out.println("o的值是:"+o);
return o;
}
//執行原來方法
Object proceed = joinPoint.proceed();
//放入快取
useRedisCache(redisCacheAble, handler, proceed);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
/**
* 切的方法。這樣寫是遇到這個註解的時候AOP來處理,為專案解耦是一方面
* @param joinPoint
* @param redisCacheEvict
* @return
*/
@Around("@annotation(redisCacheEvict)")
public Object handlers(ProceedingJoinPoint joinPoint, RedisCacheEvict redisCacheEvict) {
Object object=null;
try {
String handler = returnRedisKey(joinPoint, redisCacheEvict.key(), redisCacheEvict.cacheName());
System.out.println("刪除的鍵:"+handler);
if (redisCacheEvict.isHash()) {
jedis.hdel(handler, redisCacheEvict.field());
} else {
jedis.del(handler);
}
object=joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return object;
}
/**
* 使用redis快取,這個不該寫在這的。。為了簡單起見。就混入這了
* @param redisCacheAble
* @param redisKeyName
* @param redisValue
* @throws Exception
*/
private void useRedisCache(RedisCacheAble redisCacheAble, String redisKeyName, Object redisValue) throws Exception {
int time = redisCacheAble.time();
if (redisCacheAble.isHash()) {
if (time != -1) {
System.out.println("插入雜湊快取(有時間)");
String field = redisCacheAble.field();
jedis.hset(redisKeyName, field, JSON.toJSONString(redisValue));
jedis.expire(redisKeyName, time);
} else {
System.out.println("插入雜湊快取");
String field = redisCacheAble.field();
jedis.hset(redisKeyName, field, JSON.toJSONString(redisValue));
}
} else {
if (time != -1) {
System.out.println("插入快取(有時間)");
jedis.set(redisKeyName, JSON.toJSONString(redisValue));
jedis.expire(redisKeyName, time);
} else {
System.out.println("插入快取");
jedis.set(redisKeyName,JSON.toJSONString(redisValue));
}
}
}
/**
* 獲取redis快取的值,這個不該寫在這的。。為了簡單起見。就混入這了
* @param redisCacheAble
* @param object
* @return
* @throws Exception
*/
private String getRedisCacheValue(RedisCacheAble redisCacheAble, String object) throws Exception {
if (redisCacheAble.isHash()) {
String field = redisCacheAble.field();
System.out.println(object + " " + field);
return jedis.hget(object, field);
} else {
return jedis.get(object);
}
}
/**
* 主要的任務是將生成的redis key返回
* @param joinPoint
* @param keys
* @param cacheName
* @return
* @throws Exception
*/
private String returnRedisKey(ProceedingJoinPoint joinPoint, String keys, String cacheName) throws Exception {
boolean b = checkKey(keys);
if (!b) {
throw new RuntimeException("鍵規則有錯誤或鍵為空");
}
String key = getSubstringKey(keys);
//判定是否有. 例如#user.id 有則要處理,無則進一步處理
if (!key.contains(".")) {
Object arg = getArg(joinPoint, key);
//判定請求引數中是否有相關引數。無則直接當鍵處理,有則取值當鍵處理
String string;
if (arg == null) {
string = handlerRedisKey(cacheName, key);
} else {
string = handlerRedisKey(cacheName, arg);
}
return string;
} else {
//拿到物件引數 例如 user.id 拿到的是user這個相關物件
Object arg = getArg(joinPoint, handlerIncludSpot(key));
Object objectKey = getObjectKey(arg, key.substring(key.indexOf(".") + 1));
return handlerRedisKey(cacheName, objectKey);
}
}
private String handlerRedisKey(String cacheName, Object key) {
return cacheName + Default_String + key;
}
/**
* 遞迴找到相關的引數,並最終返回一個值
*
* @param object 傳入的物件
* @param key key名,用於拼接成 get+key
* @return 返回處理後拿到的值 比如 user.id id的值是10 則將10返回
* @throws Exception 異常
*/
private Object getObjectKey(Object object, String key) throws Exception {
//判斷key是否為空
if (StringUtils.isEmpty(key)) {
return object;
}
//拿到user.xxx 例如:key是user.user.id 遞迴取到最後的id。並返回數值
int doIndex = key.indexOf(".");
if (doIndex > 0) {
String propertyName = key.substring(0, doIndex);
//擷取
key = key.substring(doIndex + 1);
Object obj = getProperty(object, getMethod(propertyName));
return getObjectKey(obj, key);
}
return getProperty(object, getMethod(key));
}
/**
* 也是擷取字串。沒好說的
*
* @param key 傳入的key
*/
private String handlerIncludSpot(String key) {
int doIndex = key.indexOf(".");
return key.substring(0, doIndex);
}
/**
* 獲取某方法中的返回值。。例如:public int getXXX() 拿到的是返回int的的數值
*
* @param object 物件例項
* @param methodName 方法名
* @return 返回通過getXXX拿到屬性值
* @throws Exception 異常
*/
private Object getProperty(Object object, String methodName) throws Exception {
return object.getClass().getMethod(methodName).invoke(object);
}
/**
* 返回擷取的的字串
*
* @param keys 用於擷取的鍵
* @return 返回擷取的的字串
*/
private String getSubstringKey(String keys) {
//去掉# ,在設定例如 #user 變成 user
return keys.substring(1).substring(0, 1) + keys.substring(2);
}
/**
* 獲得get方法,例如拿到了User物件,拿他的setXX方法
*
* @param key 鍵名,用於拼接
* @return 方法名字(即getXXX() )
*/
private String getMethod(String key) throws Exception {
return "get" + Character.toUpperCase(key.charAt(0)) + key.substring(1);
}
/**
* 獲取請求的引數。
*
* @param joinPoint 切點
* @param paramName 請求引數的名字
* @return 返回和引數名一樣的引數物件或值
* @throws NoSuchMethodException 異常
*/
private Object getArg(ProceedingJoinPoint joinPoint, String paramName) throws NoSuchMethodException {
Signature signature = joinPoint.getSignature();
//獲取請求的引數
MethodSignature si = (MethodSignature) signature;
Method method0 = joinPoint.getTarget().getClass().getMethod(si.getName(), si.getParameterTypes());
ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] p = parameterNameDiscoverer.getParameterNames(method0);
if (p == null) {
throw new IllegalArgumentException("沒有引數[" + paramName + "] 沒有方法:" + method0);
}
//判斷是否有相關引數
int indix = 0;
for (String string : p) {
if (string.equalsIgnoreCase(paramName)) {
return joinPoint.getArgs()[indix];
}
indix++;
}
return null;
}
/**
* 鍵規則檢驗 是否符合開頭#
*
* @param key 傳入的key
* @return 返回是否包含
*/
private boolean checkKey(String key) {
if (StringUtils.isEmpty(key)) {
return false;
}
String temp = key.substring(0, 1);
//如果沒有以#開頭,報錯
return temp.equals("#");
}
/**
* 方法不支援返回值是集合型別 例如List<User> 無法獲取集合中的物件。
* 支援物件,基本型別,引用型別
* @param joinPoint
* @return
*/
private Class<?> getMethodReturnType(ProceedingJoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
Class declaringType = signature.getDeclaringType();
String name = signature.getName();
Method[] methods = declaringType.getMethods();
for (Method method :methods){
if (method.getName().equals(name)){
Class<?> returnType = method.getReturnType();
System.out.println("返回值型別:"+returnType);
return returnType;
}
}
throw new RuntimeException("找不到返回引數。請檢查方法返回值是否是物件型別");
}
}
測試:
專案原始碼:
總結:
專案中後期加入,不想造成業務入侵,所以編寫了這個適合位元組業務的簡單專案。當然現在也有現成的註解,而且比較成熟,可以使用。