SpringBoot如何解析注解里的动态参数?

在SpringBoot中,我们经常会使用自定义注解+AOP的方式来封装一些重复的操作,例如方法的参数校验,获取分布式锁等。

如果我们需要在注解中动态的传入参数,例如在加锁的操作中,需要根据方法的入参动态的传入userId作为lock的key,这个动态参数在aspect类中应该如何解析出来呢?答案是利用SpEL表达式实现,下面以一个简单的例子说明具体用法。

SpEL表达式实现注解动态参数

首先,设计一个自定义的注解:

@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicParam {

    String value() default "";

    // 用于传入动态参数
    String key() default "";

}

然后,在方法上使用这个注解:

@DynamicParam(value = "演示注解用法", key = "#user.id")
public void testMethod(User user){
   // do something...
}

 最后,设计一个aspect来从注解中解析出user.id:

@Aspect
@Component
@Slf4j
public class SpelAspect {

    private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    @Around(value = "@annotation(dynamicParam)")
    public Object lock(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) throws Throwable {
        // 解析出注解里的动态参数
        String param = getDynamicParam(joinPoint, dynamicParam);
        try{
            // do something...
            return joinPoint.proceed();
        }catch (Throwable e) {
            log.error(e.getMessage(),e);
            return null;
        }
    }

    /**
     * 解析出注解里的动态参数
     * @param joinPoint
     * @param dynamicParam
     * @return
     */
    private String getDynamicParam(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) {
        StringBuilder sb = new StringBuilder();
        // spel解析器
        ExpressionParser parser = new SpelExpressionParser();
        // spel表达式对象
        Expression expression = parser.parseExpression(dynamicParam.key());
        // 上下文对象
        EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, resolveMethod(joinPoint),
                joinPoint.getArgs(), parameterNameDiscoverer);
        // 解析出的动态参数
        Object value = expression.getValue(context);
        sb.append(ObjectUtils.nullSafeToString(value));
        return sb.toString();
    }

    /**
     * 获取注解所在的method
     * @param joinPoint
     * @return
     */
    private Method resolveMethod(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Class<?> targetClass = joinPoint.getTarget().getClass();

        return getDeclaredMethodFor(targetClass, signature.getName(),
                signature.getMethod().getParameterTypes());
    }

    private Method getDeclaredMethodFor(Class<?> clazz, String name, Class<?>... parameterTypes) {
        try {
            return clazz.getDeclaredMethod(name, parameterTypes);
        } catch (NoSuchMethodException e) {
            Class<?> superClass = clazz.getSuperclass();
            if (superClass != null) {
                return getDeclaredMethodFor(superClass, name, parameterTypes);
            }
        }
        throw new IllegalStateException("Cannot resolve target method: " + name);
    }

}

 这样就可以顺利的从自定义的注解中解析出动态参数user.id了。

解析SpEL的过程分析

上面案例中,最核心的是getDynamicParam方法:

private String getDynamicParam(ProceedingJoinPoint joinPoint, DynamicParam dynamicParam) {
        StringBuilder sb = new StringBuilder();
        // spel解析器
        ExpressionParser parser = new SpelExpressionParser();
        // spel表达式对象
        Expression expression = parser.parseExpression(dynamicParam.key());
        // 上下文对象
        EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, resolveMethod(joinPoint),
                joinPoint.getArgs(), parameterNameDiscoverer);
        // 解析出的动态参数
        Object value = expression.getValue(context);
        sb.append(ObjectUtils.nullSafeToString(value));
        return sb.toString();
    }

 

这个方法比较清晰的体现了解析SpEL表达式的过程:

1.构建一个SpEL表达式的解析器

2.利用解析器把注解中传入的SpEL表达式解析成一个表达式对象

3.构建一个上下文对象,它是表达式对象的执行环境

4.从上下文对象中,解析出动态参数

为了方便理解,可以把上述过程做进一步说明:

我们在注解中传入的SpEL表达式相当于一个指令,解析器就是要执行这个指令的人,上下文对象就是这个人的工作环境,他只有在这个工作环境中执行这个命令,才能拿到我们想要的结果。

代码中一些类的说明

ExpressionParser接口:

它表示解析器,这个接口有3个实现类,其中TemplateAwareExpressionParser implements ExpressionParser

另外两个则是继承了TemplateAwareExpressionParser

SpelExpressionParser是这个接口的默认实现类

EvaluationContext接口:

它表示的是上下文环境,它的默认实现类是StandardEvaluationContext,我们在代码中使用的MethodBasedEvaluationContext继承自StandardEvaluationContext。

当我们用MethodBasedEvaluationContext构建出一个上下文环境context后,它的内部就会包含这个method的信息以及method入参的具体值,这也是为什么解析器必须在这个环境中才能解析出我们想要的结果。

Expression接口:

它表示SpEL表达式对象,默认实现类是SpelExpression。

表达式对象的getValue用于获取表达式的值,setValue方法则用于设置根对象或上下文环境中的值。

ParameterNameDiscoverer接口:

在获取Method对象的过程中,我们还用到了ParameterNameDiscoverer接口,Spring用它来获取方法中的参数的名称,这里只是简单使用,没有做深究。

希望上面的内容能给你带来帮助!

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值