当注解使用在方法上时,注解里怎么能够拿到方法上的参数值呢?如果你使用过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("==== 执行业务逻辑完成 ====");
}
运行结果
运行日志:
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 : ==== 执行业务逻辑完成 ====