对于常常访问的数据,直接用Redis缓存可以很好的减少数据库压力,提升响应速度,实现思路如下
- 利用注解,标注于需要缓存的方法上,利用AOP截取进行处理
- 缓存的key名称可为SpringEL表达式,可选是否转为MD5减少key长度,方法的结果则为转换为JSON,所以注意Redis在序列化器要选择Json而不是String
- 在AOP的环绕通知中,如果缓存存在直接返回该缓存,不存在则缓存当前方法的结果
- 在进行增删改操作的方法上添加删除缓存的注解,及时清除对应的缓存
首先先新建三个注解,分别是
- EnableCache:开启缓存
- 参数expireTime:过期时间
- 参数useMd5Key:是否使用MD5加密key
- 参数keyName:key名称,可为EL表达式,格式#{#param} or #{#param.value}
- 参数condition:缓存条件,EL表达式,比如#{#param.value>5}
- DisableCache:禁用缓存
- DeleteCache:删除缓存
- 参数useMd5Key:是否使用MD5加密key
- 参数keyName:key名称,可为EL表达式,格式#{#param} or #{#param.value}
- 参数condition:缓存条件,EL表达式,比如#{#param.value>5}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableCache {
/**
* 缓存时间,单位(秒)
*
* @return
*/
long expireTime() default 60 * 60L;
/**
* 是否使用MD5的key名称
*
* @return
*/
boolean useMd5Key() default false;
/**
* 自定义key名称,可为EL表达式,格式#{#param} or #{#param.value}
*
* @return
*/
String keyName() default "";
/**
* 缓存条件,EL表达式,比如#{#param.value>5}
*
* @return
*/
String condition() default "";
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeleteCache {
/**
* 自定义key名称,可为EL表达式,格式#{#param} or #{#param.value}
*
* @return
*/
String keyName() default "";
/**
* 是否使用MD5的key名称
*
* @return
*/
boolean useMd5Key() default false;
/**
* 删除缓存条件,EL表达式,比如#{#param.value>5}
*
* @return
*/
String condition() default "";
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableCache {
}
然后需要一个工具类SpringExpressionUtil用于解析Spring EL表达式
/**
* @author : LoneKing
*/
public class SpringExpressionUtil {
/**
* 转换EL表达式结果
*
* @param key
* @param method
* @param args
* @param useMd5Key
* @return key
*/
public static String parseKey(String key, Method method, Object[] args, boolean useMd5Key) {
Object returnVal = parse(key, method, args);
//这块这么做,是为了Object和String都可以转成String类型的,可以作为key
String jsonKey = JsonUtil.serialize(returnVal);
//转换成md5,是因为redis的key过长,并且这种大key的数量过多,就会占用内存,影响性能
if (useMd5Key) {
jsonKey = SecurityUtil.getMD5(jsonKey);
}
return returnVal == null ? null : jsonKey;
}
/**
* 获取EL表达式(条件)结果
*
* @param key
* @param method
* @param args
* @return true or false
*/
public static boolean parseCondition(String expression, Method method, Object[] args) {
if (StringUtils.isEmpty(expression)) {
return true;
}
Object returnVal = parse(expression, method, args);
return Objects.equals(returnVal, true);
}
/**
* 获取EL表达式结果
*
* @param expression
* @param method
* @param args
* @return
*/
public static Object parse(String expression, Method method, Object[] args) {
// 获取被拦截方法参数名列表
LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] paramNameArr = parameterNameDiscoverer.getParameterNames(method);
// 使用SpringEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
// SpringEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
// 把方法参数放入SpringEL上下文中
for (int i = 0; i < paramNameArr.length; i++) {
context.setVariable(paramNameArr[i], args[i]);
}
ParserContext parserContext = new TemplateParserContext();
Object returnVal = parser.parseExpression(expression, parserContext)
.getValue(context, Object.class);
return returnVal;
}
}
最后就是重要的AOP了,没什么可说的,其中用到的RedisUtil代码就不放了,添加/删除Redis缓存。
注意:如果在控制器上添加EnableCache注解,代表其下所有方法开启缓存,就不要设置keyName这个参数了,默认值为全类名_方法名_参数序列化
/**
* @author : LoneKing
*/
@Component
@Aspect
@Slf4j
public class RedisCacheAspect {
private final RedisUtil redisUtil;
public RedisCacheAspect(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
/**
* 开启缓存的切点
*/
@Pointcut("@within(com.loneking.annotation.EnableCache) || @annotation(com.loneking.annotation.EnableCache)")
private void cachePointcut() {
}
/**
* 删除缓存的切点
*/
@Pointcut("@within(com.loneking.annotation.DeleteCache) || @annotation(com.loneking.annotation.DeleteCache)")
private void deleteCachePointcut() {
}
/**
* 环绕通知 开启缓存
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("cachePointcut()")
public Object enableCache(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Signature sig = joinPoint.getSignature();
MethodSignature ms = (MethodSignature) sig;
Method method = target.getClass().getMethod(ms.getName(), ms.getParameterTypes());
var cacheModel = getCacheModel(method, joinPoint);
Object result;
try {
//如果允许缓存
if (cacheModel.getAllowCache()) {
//如果缓存条件通过
if (cacheModel.getCacheCondition()) {
//获取redis缓存内容
Object cacheData = redisUtil.get(cacheModel.getKeyName());
if (cacheData != null) {
result = cacheData;
log.info("触发缓存:" + cacheModel.getKeyName());
} else {
result = joinPoint.proceed();
redisUtil.set(cacheModel.getKeyName(), result, cacheModel.getExpireTime());
}
} else {
result = joinPoint.proceed();
}
} else {
result = joinPoint.proceed();
}
} catch (SystemException e) {
throw e;
} catch (Exception e) {
throw e;
}
return result;
}
/**
* 环绕通知 删除缓存
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("deleteCachePointcut()")
public Object deleteCache(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
Signature sig = joinPoint.getSignature();
MethodSignature ms = (MethodSignature) sig;
Method method = target.getClass().getMethod(ms.getName(), ms.getParameterTypes());
DeleteCache deleteCache = AnnotationUtils.findAnnotation(method, DeleteCache.class);
if (deleteCache != null) {
//转换Spring EL表达式
var delCacheCondition = SpringExpressionUtil.parseCondition(
deleteCache.condition(), method, joinPoint.getArgs());
if (delCacheCondition) {
String keyName = getKeyName(deleteCache.keyName(), deleteCache.useMd5Key(), method, joinPoint);
redisUtil.del(keyName);
log.info("删除缓存:" + keyName);
}
}
Object result;
result = joinPoint.proceed();
return result;
}
/**
* 获取key
*
* @param useMd5Key
* @param method
* @param joinPoint
* @return
* @expressionKeyName key名称,可为EL表达式
*/
private String getKeyName(String expressionKeyName, boolean useMd5Key, Method method, ProceedingJoinPoint joinPoint) {
String defaultKeyName = joinPoint.getTarget().getClass().toString() + "_" + method.getName() + "_"
+ JsonUtil.serialize(joinPoint.getArgs());
if (StringUtils.isNotEmpty(expressionKeyName)) {
//转换Spring EL表达式
return SpringExpressionUtil.parseKey(
expressionKeyName, method, joinPoint.getArgs(), useMd5Key);
} else {
if (useMd5Key) {
return SecurityUtil.getMD5(defaultKeyName);
} else {
return defaultKeyName;
}
}
}
/**
* 获取方法注解的缓存信息
*
* @param method
* @param joinPoint
* @return
*/
private RedisCacheModel getCacheModel(Method method, ProceedingJoinPoint joinPoint) {
//获取方法上的开启缓存注解
EnableCache methodEnableCache = AnnotationUtils.findAnnotation(method, EnableCache.class);
//获取方法上的禁用缓存注解
DisableCache methodDisableCache = AnnotationUtils.findAnnotation(method, DisableCache.class);
//获取方法所在类的开启缓存注解
EnableCache controllerEnableCache = AnnotationUtils.findAnnotation(method.getDeclaringClass(), EnableCache.class);
//是否启用缓存 该方法未禁用缓存 且 方法/控制器上有开启缓存的注解
var allowCache = methodDisableCache == null && (controllerEnableCache != null || methodEnableCache != null);
Type methodReturnType = method.getAnnotatedReturnType().getType();
//不缓存void返回类型的方法
allowCache = allowCache && !"void".equals(methodReturnType.getTypeName());
EnableCache usedEnableCache = null;
boolean cacheCondition = false;
long expireTime = 1L;
String keyName = "";
//如果允许缓存
if (allowCache) {
//优先获取方法上的缓存注解
usedEnableCache = methodEnableCache != null ? methodEnableCache : controllerEnableCache;
//获取过期时间
expireTime = usedEnableCache.expireTime();
keyName = getKeyName(usedEnableCache.keyName(), usedEnableCache.useMd5Key(), method, joinPoint);
//转换Spring EL表达式条件
cacheCondition = SpringExpressionUtil.parseCondition(
usedEnableCache.condition(), method, joinPoint.getArgs());
}
return new RedisCacheModel(cacheCondition, allowCache, keyName, expireTime);
}
@Data
@AllArgsConstructor
private class RedisCacheModel {
/**
* 缓存条件
*/
private Boolean cacheCondition;
/**
* 允许缓存
*/
private Boolean allowCache;
/**
* key名称
*/
private String keyName;
/**
* 过期时间,单位(秒)
*/
private Long expireTime;
}
}
食用方法如下
@Controller
public class HomeController {
@GetMapping("/cache")
@ResponseBody
@EnableCache(keyName = "home_cache_#{#param}", condition = "#{#param!='3'}") //开启缓存
public String cache(String param) {
return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e"});
}
@GetMapping("/delCache")
@ResponseBody
@DeleteCache(keyName = "home_cache_#{#param}", condition = "#{#param!='5'}") //删除缓存
public String delCache(String param) {
return Result.build().successJson();
}
}
访问/cache就会发现redis缓存本次结果了了,二次访问就会直接调用redis的缓存
或者
@Controller
@EnableCache //直接在控制器上开启缓存,其下所有方法将缓存,除非用了DisableCache
public class HomeController {
@GetMapping("/editor")
public String editor() {
return "/modeler";
}
@GetMapping("/cache")
@ResponseBody
public String cache(String param) {
return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e"});
}
@GetMapping("/cache2")
@ResponseBody
@DisableCache
public String cache2(String param) {
return JsonUtil.serialize(new String[]{"c", "a", "c", "h", "e", "2"});
}
}
这样的话每个方法所缓存的key名称就是全类名_方法名_参数序列化
如果设置了@EnableCache(useMd5Key = true),那么key如下