spring-data-redis虽是强大,但是许多功能用不上,我只需要简单的缓存添加、删除功能,于是基于自定义注解和AOP知识实现了3个缓存注解。
关键点
1、@Around("@annotation(myCacheAnnotation.Cache)"),使用spring的@Around注解,对我自定义的注解类Cache进行切入,也就是说,凡是使用到@Cache这个注解的方法,执行时会先执行@Around下的方法,这样就可以把之前写在业务方法中的缓存逻辑移动到这里,比如获取数据前先到redis服务器获取缓存,缓存不存在再到数据库中去获取;
2、Method m = ((MethodSignature) pjp.getSignature()).getMethod(); 这行代码中的m是代理对象,没有包含原方法上的注解;Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());ProceedingJoinPoint(上面的pjp)的this()返回spring生成的代理对象,target()返回被代理的目标对象,目标对象反射获取的method对象才包含注解;
3、java本身的反射功能中不能获取方法参数名,借助LocalVariableTableParameterNameDiscoverer类可以根据一个Method对象获取其方法参数名。
核心代码
AOP切面类
package myCacheAnnotation;
import com.alibaba.fastjson.JSON;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import service.IUserService;
import java.lang.reflect.Method;
/**
* writer: holien
* Time: 2018-01-18 11:20
* Intent: aop配合cache注解实现缓存
*/
@Component
@Aspect
public class CacheAspect {
@Autowired
private JedisPool jedisPool;
// 在使用Cache注解的地方切入此切点
@Around("@annotation(myCacheAnnotation.Cache)")
private Object handleCache(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("以下是缓存逻辑");
// 获取切入的方法对象
// 这个m是代理对象的,没有包含注解
Method m = ((MethodSignature) pjp.getSignature()).getMethod();
// this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
// 根据目标方法对象获取注解对象
Cache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(myCacheAnnotation.Cache.class);
// 解析key
String keyExpr = cacheAnnotation.key();
Object[] as = pjp.getArgs();
String key = parseKey(methodWithAnnotations, as, keyExpr);
// 注解的属性本质是注解里的定义的方法
// Method methodOfAnnotation = a.getClass().getMethod("key");
// 注解的值本质是注解里的定义的方法返回值
// String key = (String) methodOfAnnotation.invoke(a);
// 到redis中获取缓存
Jedis jedis = jedisPool.getResource();
String cache = jedis.get(key);
if (cache == null) {
// 若不存在,则到数据库中去获取
Object result = pjp.proceed();
// 从数据库获取后存入redis
System.out.println("从数据库获取的结果以JsonString形式存入redis中");
jedis.set(key, JSON.toJSONString(result));
// 若有指定过期时间,则设置
int expireTime = cacheAnnotation.expire();
if (expireTime != -1) {
jedis.expire(key, expireTime);
}
return result;
} else {
return JSON.parse(cache);
}
}
// 参数2为方法参数值,参数3为注解中某个属性的值,若含有#则为一个表达式
// 待解决,解析#user.id的问题
private String parseKey(Method method, Object[] argValues, String expr) {
if (expr.contains("#")) {
String paramName = expr.substring(expr.indexOf('#') + 1);
// 获取方法参数名列表
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = discoverer.getParameterNames(method);
for (int i = 0; i < paramNames.length; i++) {
if (paramNames[i].equals(paramName)) {
return expr.substring(0, expr.indexOf('#')) + argValues[i].toString();
}
}
throw new IllegalArgumentException("解析不了该参数,错误参数表达式");
} else {
// 不需要解析,直接返回
return expr;
}
}
// 待解决,解析#user.id的问题
@Around("@annotation(myCacheAnnotation.CachePut)")
private Object handleCachePut(ProceedingJoinPoint pjp) throws Throwable {
return null;
}
@Around("@annotation(myCacheAnnotation.CacheEvict)")
private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("以下是删除缓存逻辑");
// 获取切入的方法对象
// 这个m是代理对象的,没有包含注解
Method m = ((MethodSignature) pjp.getSignature()).getMethod();
// this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
// 根据目标方法对象获取注解对象
CacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(myCacheAnnotation.CacheEvict.class);
// 解析key
String keyExpr = cacheEvictAnnotation.key();
Object[] as = pjp.getArgs();
String key = parseKey(methodWithAnnotations, as, keyExpr);
// 先删除数据库中的用户信息再删除缓存
Object result = pjp.proceed();
Jedis jedis = jedisPool.getResource();
jedis.del(key);
System.out.println("删除缓存中的用户信息");
return result;
}
public static void main(String[] args) throws Exception {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
IUserService userService = (IUserService) context.getBean("userService");
// System.out.println(userService.getUserInfo(10));
userService.deleteUserInfo(10);
}
}
Cache注解类
package myCacheAnnotation;
import java.lang.annotation.*;
/**
* writer: holien
* Time: 2018-01-18 10:49
* Intent: 自定义缓存注解(String、hash)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
String key();
int expire() default -1;
}
不足与补充
此注解还可以使用Guava包中的布隆过滤器,对数据库和缓存中都不存在的查询放进过滤器,防止缓存击穿攻击;本来想借助SpEL来解析注解参数值,但是没有试验成功,只好自己写了个简单的对#XXX格式的参数进行解析,XXX只能是方法的其中一个参数名,不能是参数的属性或方法返回值,即不能解析#user.id或#user.getId();
总结
其实重复造轮子是没有必要的,但是以学习或特定业务为目的造个小轮子是值得的,这次的学习也让我体会到AOP和注解的强大之处,站在伟人的肩膀上看得更远。