在注解中使用方法参数值


当注解使用在方法上时,注解里怎么能够拿到方法上的参数值呢?如果你使用过spring cache就知道,我们可以使用SpEL表达式拿到方法参数,那么我们自定义的注解怎么实现类似的功能呢?

之前我们在Spring-Boot-2实现分布式锁讲过了在spring boot一种分布式锁的使用方法。但是每次使用时我们都需要硬编写一些模板代码,如下:

private final RedisLockRegistry redisLockRegistry;    

public void lockTest(String name) throws InterruptedException {
    // name 作为 lockKey
    final Lock obtain = redisLockRegistry.obtain(name);
    log.info("获取锁: {}", obtain);
    if (obtain.tryLock()) {
        log.info("==== 执行业务逻辑 ====");
        try {
            TimeUnit.SECONDS.sleep(30);
        } finally {
            obtain.unlock();
        }
    } else {
        log.warn("==== 获取锁失败 ====");
    }
}

这里获取锁,释放锁的代码基本都是固定的,为了避免每次写类似的代码,我们可以利用aop将固定的模板代码抽取出来,而我们的方法只关注业务逻辑!要这样做我们可以参考spring cache的实现,我们自定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {

    /**
     * 分布式锁key
     */
    String key();

    /**
     * 获取锁失败提示
     */
    String failMessage() default "";
    
}

但是有个问题,注解里面的key我们不能定义死,这样就会对标记的方法加同一把锁,但是实际情况下,同一个方法,我们需要根据参数来确定使用什么作为lockKey.既然key要动态获取方法参数,怎么才能拿到方法参数呢?怎么定义key的表达式呢?这些问题我们不需要全部自己实现,spring里已提供了解决方案。要拿到方法的参数名称,在java8之前是没法直接通过反射获取到的,在java8编译的时候使用-parameters,可以在 编译的时候生成元数据以用于方法参数的反射。但是在java8之前就要通过比如ASM library 工具分许class文件,拿到对应的参数名称。不管什么情况,我们可以统一使用spring提供ParameterNameDiscoverer来获取参数名称,该接口有一个实现DefaultParameterNameDiscoverer,该解析类提供了两个解析方法参数的类,一个使用java反射的StandardReflectionParameterNameDiscoverer,一个使用ASM library LocalVariableTableParameterNameDiscoverer,当第一个获取为null的时候就是用第二个来获取。解决了拿到方法参数的问题之后,我们怎么能拿到方法参数里面的值呢,我们可以使用spel!直接看代码!

定义切面

@Slf4j
@Aspect
@Component
public class DistributedLockAspect {

    @Autowired
    private RedisLockRegistry redisLockRegistry;

    private final SpelExpressionParser parser = new SpelExpressionParser();

    private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    @Around(value = "within(com.example.demo20201212..*) && @annotation(distributedLock)", argNames = "distributedLock")
    public Object distributedLockAround(ProceedingJoinPoint proceedingJoinPoint, DistributedLock distributedLock) throws Throwable {
        Assert.hasLength(distributedLock.key(), "key不能为空");
        // 获取方法参数
        Object[] args = proceedingJoinPoint.getArgs();
        // 获取方法参数名称
        Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
        // 构建spel上下文
        SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        for (int i = 0; i < args.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }

        // 解析spel参数
        Expression expression = parser.parseExpression(distributedLock.key());
        String lockKey = expression.getValue(context, String.class);
        Assert.hasLength(lockKey, "lockKey不能为空");

        // 加分布式锁
        Lock obtain = redisLockRegistry.obtain(lockKey);
        log.info("开始获取锁: {}", lockKey);
        if (obtain.tryLock()) {
            try {
                // 执行业务逻辑
                return proceedingJoinPoint.proceed();
            } finally {
                // 释放`锁
                obtain.unlock();
            }
        } else {
            log.warn("==== 获取锁失败 ====");
            throw new RuntimeException(StringUtils.hasLength(distributedLock.failMessage()) ? distributedLock.failMessage() : "获取锁失败");
        }

    }
}

拦截方法

@DistributedLock(key = "#name") // 使用 spel 表达式
public void lockTest(String name) throws InterruptedException {
  log.info("==== 执行业务逻辑开始 ====");
  TimeUnit.SECONDS.sleep(5);
  log.info("==== 执行业务逻辑完成 ====");
}

运行结果

image-20210222151207651

运行日志:

2021-02-22 15:12:27.333  INFO [demo,370811965c990d14,370811965c990d14] 40326 --- [nio-8080-exec-3] c.e.d.support.DistributedLockAspect      : 开始获取锁: shiv
2021-02-22 15:12:27.334  WARN [demo,,] 40326 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=3m22s450ms).
2021-02-22 15:12:27.404  INFO [demo,370811965c990d14,370811965c990d14] 40326 --- [nio-8080-exec-3] c.e.d.service.OrderQueryService          : ==== 执行业务逻辑开始 ====
2021-02-22 15:12:32.409  INFO [demo,370811965c990d14,370811965c990d14] 40326 --- [nio-8080-exec-3] c.e.d.service.OrderQueryService          : ==== 执行业务逻辑完成 ====

文章作者: shiv
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 shiv !
评论
  目录