1. 程式人生 > >隨筆(八) 自定義redis快取註解(基於springboot)

隨筆(八) 自定義redis快取註解(基於springboot)

前言:

           最近專案開發中需要使用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("找不到返回引數。請檢查方法返回值是否是物件型別");
  }
}

測試:

                    

                      

專案原始碼:

總結:

        專案中後期加入,不想造成業務入侵,所以編寫了這個適合位元組業務的簡單專案。當然現在也有現成的註解,而且比較成熟,可以使用。

程式人生,與君共勉~!