首页 > 编程知识 正文

spel表达式注入

时间:2023-05-06 16:20:24 阅读:183273 作者:3105

引子

我现在负责项目中,数据计算量比较大,有强烈的缓存需求。但是无奈,我司的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表达式解析。

@Servicepublic 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> T parse(String expression, Method method, Class<T> 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();}

然后再在切面中处理具体的逻辑:

@Resourceprivate CacheToolService cacheToolService;@Resourceprivate 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<String> 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

总结

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

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。