注解实现动态锁业务

概要

基于注解、Spring AOPSpring Expression表达式实现动态锁业务。

代码实现

FormLock.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 防止表单重复提交锁实现注解
*
* @author yl
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FormLock {
/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
* <p>
* 表单锁 key 的值,spring el 表达式(Spring Expression Language (SpEL) expression),动态获取表单锁的 key 值
*/
String key() default "";
}

FormLockAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
* @author yl
*/
@Aspect
@Slf4j
@Component
@RequiredArgsConstructor
public class FormLockAspect {
private final RedissonClient redissonClient;

/**
* 锁
*/
private static final String LOCK_KEY = "redisson::lock::%s::%s";

private final ExpressionParser parser = new SpelExpressionParser();
private final LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();

@Pointcut("@annotation(com.grgbanking.prms.common.lock.FormLock)")
public void pointcut() {
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Method method = resolveMethod(point);
FormLock formLock = AnnotationUtils.findAnnotation(method, FormLock.class);
if (formLock == null) {
return point.proceed();
}
String el = formLock.key();
if (StringUtils.isBlank(el)) {
return point.proceed();
}
if (!el.startsWith("#")) {
return point.proceed();
}
Object[] args = point.getArgs();

// Expose indexed variables as well as parameter names (if discoverable)
String[] paramNames = this.discoverer.getParameterNames(method);
int paramCount = (paramNames != null ? paramNames.length : method.getParameterCount());
int argsCount = args.length;

EvaluationContext eval = new StandardEvaluationContext();
for (int i = 0; i < paramCount; i++) {
Object value = null;
if (argsCount > paramCount && i == paramCount - 1) {
// Expose remaining arguments as vararg array for last parameter
value = Arrays.copyOfRange(args, i, argsCount);
} else if (argsCount > i) {
// Actual argument found - otherwise left as null
value = args[i];
}
/**
* see {@link MethodBasedEvaluationContext#lazyLoadArguments()}
*/
eval.setVariable("a" + i, value);
eval.setVariable("p" + i, value);
if (paramNames != null) {
eval.setVariable(paramNames[i], value);
}
}
Expression expression = parser.parseExpression(el);
/**
* 这里可能会抛出异常,不用处理,这样有利于程序发现{@link FormLock#key()}配置错误问题
*/
String lockKey;
try {
lockKey = expression.getValue(eval, String.class);
} catch (EvaluationException e) {
throw new EvaluationException(String.format("%s 表达式错误", el));
}
if (StringUtils.isBlank(lockKey)) {
log.warn("{} 表达式的值为空,直接执行逻辑", el);
return point.proceed();
}
String name = point.getTarget().getClass().getSimpleName() + "#" + method.getName();
RLock lock = null;
try {
lock = redissonClient.getLock(String.format(LOCK_KEY, name, lockKey));
// 如果不主动释放锁,300 秒后自动释放
boolean success = lock.tryLock(100L, 300000L, TimeUnit.MILLISECONDS);
if (!success) {
// todo 改成自己的返回格式:json、页面等
return Rs.error("表单已提交,不可重复提交");
}
// log.info("获得表单执行锁 ---> 结束,获取数量:{}", notifications.size());
return point.proceed();
} finally {
if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}

private Method resolveMethod(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Class<?> targetClass = point.getTarget().getClass();

Method method = getDeclaredMethod(targetClass, signature.getName(),
signature.getMethod().getParameterTypes());
if (method == null) {
throw new IllegalStateException("无法解析目标方法: " + signature.getMethod().getName());
}
return method;
}

private Method getDeclaredMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
try {
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
return getDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 例子:会自动读取 dto.getFormId() 的值来作为锁的 key
@FormLock(key = "#dto.formId")
@RequestMapping(value = "/list", method = RequestMethod.POST)
public String list(QueryDto dto) {
// 具体业务
}

@Data
public class QueryDto {
private String formId;
private String name;
...
}
  • 本文作者: forever杨
  • 本文链接: https://blog.yl-online.top/posts/5e4e02b5.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。如果文章内容对你有用,请记录到你的笔记中。本博客站点随时会停止服务,请不要收藏、转载!