我现在负责项目中,数据计算量比较大,有强烈的缓存需求。但是无奈,我司的Redis在集群封装后,不支持“批量操作”的命令。所以,Spring Cache 框架就用不了了。我只能自己使用AOP去实现一套类似的逻辑。
在项目中,我自定义了一个注解:
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Expire {
/**
* 需要过期的Key,支持 SPEL表达式
*/
String key() default "";
/**
* 是否使当前缓存下所有key失效
*/
boolean allEntries() default false;
}
上面的@Expire 使用类似@CacheEvict,可以指定一个缓存下的所有KEY都过期,或者 指定一个KEY过期。
现在产品有这样一个需求,要求能够指定KEY使对应缓存过期。而在我们项目中,KEY是由一个方法统一控制的,所以,这里就要求 @Expire 注解在使用key 属性的时候,能够支持SPEL表达式解析。
@Service
public class CacheToolService {
/**
* 根据 方法名,参数,组装出 缓存的 key
*
* @param method 方法
* @param args 方法的参数,本身是一个 可变参数
* @return 缓存的key
*/
public String getKey(Method method, Object... args) {
// do something
return THE_KEY;
}
}
如上图,获取KEY我统一使用 cacheToolService.getKey() 方法类获取,cacheToolService 被注册成为了一个Spring bean。
最终,我们期待的效果是,@Expire 能够像下面这样使用:
@Expire(key = "@cacheToolService.getKey(#p0)")
public void flushCache(Method method) {
// flush the cache
}
我们期望最后@Expire 注解可以像 @Cacheable 那样,支持Spel表达式。那应该怎么做呢,下面我就给出我的解决方式:
@Service("SpelParseService")
public class SpelParseServiceImpl implements SpelParseService, BeanFactoryAware {
@Resource
private SpelExpressionParser spelExpressionParser;
private BeanFactory beanFactory;
@Override
public T parse(String expression, Method method, Class cls, Object... args) {
StandardEvaluationContext context = new StandardEvaluationContext();
// 将Spring 的bean上下文放入 Spel 解析的上线文中
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
// 类似于 @Cacheable 中的 root 对象、method 对象,这里我们也默认把 method、args 变量写入当前上下文中
context.setVariable("method", method);
context.setVariable("args", args);
// 下面是 支持 #p0 #p1 这样取变量
if (args != null && args.length > 0) {
for (int i = 0, len = args.length; i < len; i++) {
context.setVariable("p" + i, args[i]);
}
}
Expression exp = spelExpressionParser.parseExpression(expression, ParserContext.TEMPLATE_EXPRESSION);
return exp.getValue(context, cls);
}
@Override
public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Bean
public SpelExpressionParser spelExpressionParser() {
return new SpelExpressionParser();
}
}
上面就是我们的核心代码。这里,值得注意的是下面这一行代码:
Expression exp = spelExpressionParser.parseExpression(expression, ParserContext.TEMPLATE_EXPRESSION);
parseExpression方法,我们传入了一个ParseContext.TEMPLATE_EXPRESSION,这就说明,我们使用了模板SPEL表达式——我们写的表达式必须格式为 #{ SPEL } 这个格式,以“#”号开头,表达式被“{}”包裹。否则,我们写的表达式将会直接被解析为一个字符串。比如:
上面那一行代码如果写成:
Expression exp = spelExpressionParser.parseExpression(expression);
此时,我们写的表达式都会按照 SPEL 表达式进行解析,此时,我们就需要确保我们的表达式引用的数据存在StandardEvaluationContext上下文中!
下面是如何使用:
我们先定义好缓存过期的方法:
@Expire(key = "#{@cacheToolService.getKey(#p0)}")
public void flushCache(Method method) {
this.flushDoingProjectCache();
}
然后再在切面中处理具体的逻辑:
@Resource
private CacheToolService cacheToolService;
@Resource
private SpelParseService spelParseService;
/**
* 用于处理 缓存 过期
*/
@Before("allMethods() && expireAnnotated()")
public void expireCache(JoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Expire expire = method.getAnnotation(Expire.class);
String keySpelExpression = expire.key();
String methodName = method.getName();
String spel = expire.key();
@SuppressWarnings("unchecked")
List keyList = spelParseService.parse(spel, method, List.class, joinPoint.getArgs());
if (CollectionUtils.isEmpty(keyList)) {
log.warn("从方法 {} 上的@Expire注解的key属性的SPEL表达式中解析出的数据为空!SPEL = {}", methodName, spel);
} else {
cacheToolService.deleteByKey(keyList.toArray(new String[0]));
log.info("KEY = {} 对应的缓存删除成功!", keyList);
return;
}
log.info("没有任何缓存被清理,请关注 @Expire 注解是否正确设置了参数!");
}
至此,我们的逻辑就完毕了。文中我只给出了一些核心代码,具体的一些逻辑、完整项目请参考附件。
源码下载:https://download.csdn.net/download/zereao/11862452
GitHub:https://github.com/Zereao/SpringBucket/tree/master/spring-boot-annotation-spel
因为之前没有自己捣鼓过这个地方的东西,这里还是有一些没有考虑到的地方。如果有什么描述不完善,或者错误的地方,欢迎大家指出,一起进步~