随笔(八) 自定义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("找不到返回参数。请检查方法返回值是否是对象类型");
  }
}

测试:

           启动项目,浏览器输入:http://172.16.110.130:8080/hello   第二次访问

                    

         删除键:http://172.16.110.130:8080/delete

                      

 

项目源代码:

https://github.com/LuckyToMeet-Dian-N/testCacheAnnotion

 

总结:

        项目中后期加入,不想造成业务入侵,所以编写了这个适合字节业务的简单项目。当然现在也有现成的注解,而且比较成熟,可以使用。

 

程序人生,与君共勉~!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值