自定义注解支持SPEL表达式

引子

我现在负责项目中,数据计算量比较大,有强烈的缓存需求。但是无奈,我司的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 } 这个格式,以“#”号开头,表达式被“{}”包裹。否则,我们写的表达式将会直接被解析为一个字符串。比如:

  • "#{@cacheToolService.getKey(#p1)}"  将会按照SPEL表达式解析,最终的结果为  getKey()方法的值;
  • "@cacheToolService.getKey(#p1)"  将会按照字符串解析,最终的结果为 “"@cacheToolService.getKey(#p1)"”这个字符串。

 上面那一行代码如果写成:

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

总结

因为之前没有自己捣鼓过这个地方的东西,这里还是有一些没有考虑到的地方。如果有什么描述不完善,或者错误的地方,欢迎大家指出,一起进步~

你可能感兴趣的:(Java框架)