自定义注解支持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> 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();
}

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

@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<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
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
好的,你的问题是如何动态命名@Service注解的value值。在Spring中,我们可以使用SpEL表达式来动态地设置@Service注解的value值。 SpEL是Spring表达式语言,它提供了一种在运行时使用表达式的方式,可以在运行时计算出表达式的值。我们可以使用SpEL表达式来设置@Service注解的value值,例如: ``` @Service("#{myServiceConfig.serviceName}") public class MyService { // ... } ``` 在这个例子中,我们使用了一个SpEL表达式来动态地设置@Service注解的value值。表达式`#{myServiceConfig.serviceName}`中的myServiceConfig是一个Spring配置文件中定义的bean,它包含一个名为serviceName的属性,我们将这个属性的值用作@Service注解的value值。 我们可以在Spring配置文件中定义这个bean,例如: ``` <bean id="myServiceConfig" class="com.example.MyServiceConfig"> <property name="serviceName" value="myService"/> </bean> ``` 在这个例子中,我们定义了一个名为myServiceConfig的bean,它是一个自定义类MyServiceConfig的实例,包含一个名为serviceName的属性,其值为"myService"。这个bean可以在我们的服务类中使用SpEL表达式来动态地设置@Service注解的value值。 需要注意的是,如果我们使用SpEL表达式来动态命名@Service注解的value值,我们需要在Spring配置文件中启用SpEL解析器。我们可以通过在配置文件中添加以下配置来实现: ``` <bean class="org.springframework.context.expression.PropertyPlaceHolderConfigurer"/> ``` 这样,我们就可以动态地命名@Service注解的value值,并在应用程序中注入我们的服务类。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值