自定义Redis缓存注解
1.需求
最近在做一个APP首页多个报表,报表查询逻辑很复杂但数据并非实时的,同一个商家的多个账号进入首页都会加载报表数据。这样,就导致首页加载速度缓慢,数据库查询压力增大。为此引入Redis缓存,因接口较多(七八个),每个接口中写一遍相似的缓存处理,太重复,对代码入侵严重。在此,采取自定义Redis缓存注解结合AOP实现。
2.方案
1.自定义注解
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface EpoRedisCache {
/**
* 描述
*
* @return 描述
*/
String desc() default "";
/**
* 前缀
*
* @return 前缀
*/
String prefix() default "";
/**
* 后缀
*
* @return 后缀
*/
String suffix() default "";
/**
* 过期时间
*
* @return 过期时间
*/
long expire() default 60;
}
2.AOP
@Aspect
@Component
@Slf4j
public class EpoRedisCacheAspect {
@Autowired
private StringRedisTemplate redisTemplate;
private ExpressionParser parser = new SpelExpressionParser();
private LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Around("@annotation(redisCache)")
public Object around(ProceedingJoinPoint pjp, EpoRedisCache redisCache) throws Throwable {
String k = redisCache.prefix();
long expire = redisCache.expire();
Method method = obtainMethod(pjp);
Type genericReturnType = method.getGenericReturnType();
if (StringUtils.isNotBlank(redisCache.suffix())) {
k = StrUtil.join(StrUtil.COLON, k, parseSpel(method, pjp.getArgs(), redisCache.suffix()));
}
String v = redisTemplate.opsForValue().get(k);
Object result;
if (StringUtils.isNotBlank(v)) {
result = JSONObject.parseObject(v, genericReturnType);
} else {
result = pjp.proceed();
redisTemplate.opsForValue().set(k, JSONObject.toJSONString(result), expire, TimeUnit.SECONDS);
}
return result;
}
private Object parseSpel(Method method, Object[] args, String spel) {
String[] parameterNames = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
if (null != parameterNames) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
try {
Expression expression = parser.parseExpression(spel);
return expression.getValue(context);
} catch (Exception e) {
return StrUtil.EMPTY;
}
}
return null;
}
/**
* 根据ProceedingJoinPoint切面作用的方法
*
* @param pjp proceedingJoinPoint
* @return method
*/
private Method obtainMethod(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
if (method.getDeclaringClass().isInterface()) {
try {
method = pjp.getTarget()
.getClass()
.getDeclaredMethod(pjp.getSignature().getName(), pjp.getSignature().getDeclaringType());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
return method;
}
}
3.注解使用
@RequestMapping("/spel")
@RestController
public class SpelSampleController {
@RequestMapping(value = "/test", method = RequestMethod.GET)
@EpoRedisCache(prefix = "aaa", suffix = "#arg+'_'+#param", expire = 10, desc = "测试")
public R<SampleEntity> test(String arg, String param) {
long start = System.currentTimeMillis();
try {
//模拟查询数据库
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
SampleEntity sampleEntity = new SampleEntity("小明", 12);
System.out.println("花费时间:"+(System.currentTimeMillis()-start));
return R.data(sampleEntity);
}
}
3.问题点
系统有一千个商家,每个商家的几十个账号看到的是同一个商家的数据。也就是说,Redis的key应该能够根据不同商家有所区别。使用了注解实现缓存功能,就无法像代码中那样,直接使用参数拼接到Redis的key中。我希望能够根据参数中的查询条件,动态设置Redis的Key。查了很多资料,决定选择SPEL表达式实现。
1.SPEL表达式和EvaluationContext
后缀填的是SPEL表达式字符串,用于动态获取参数列表参数,与前缀拼接共同组成Redis缓存的key。
官方文档: 官方文档
parseExpression(“表达式”) 会获取一个Expression执行类。
getValue():获取表达式计算结果结果
getValue() returen Object :是在 默认上下文ApplicationContext中的计算结果。
getValue(EvaluationContext.class):相当于自定义上下文,限定了计算范围。
EvaluationContext 创建其子类 new StandardEvaluationContext() 创建自定义容器
evaluationContext.setVariable(key,value);将数据限制到自定义的上下文中。
2.LocalVariableTableParameterNameDiscoverer
LocalVariableTableParameterNameDiscoverer.getParameterNames(Method实例):用于获取该方法参数列表的名称数组。