1.背景
最近项目中碰到一些因为网络延迟及系统响应速度变慢造成的请求重复提交问题,之前用若依可以直接用自带的用,但是本身业务是隔离的,所以这里用自定义注解实现接口防重复调用
2.创建自定义注解
/**
* @Author:crispsea
* @Date :2024/6/20 - 06 - 20 - 15:45
*/
@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockCommit {
String key() default "";
}
@Target(ElementType.METHOD)
: 这个元注解指定了LockCommit
注解可以应用的目标类型。在这个例子中,ElementType.METHOD
表示这个注解只能应用于方法上。
@Retention(value = RetentionPolicy.RUNTIME)
: 这个元注解指定了LockCommit
注解的信息将保留到哪个阶段。RetentionPolicy.RUNTIME
表示注解会在编译后的class文件中存在,并且在运行时可以通过反射机制访问到该注解的信息。
@Documented
: 这个元注解表示当使用LockCommit
注解的方法被JavaDoc工具处理时,该注解会被包含在生成的文档中。也就是说,这个注解是可文档化的,有助于提高代码的可读性和可维护性。
@Inherited
: 这个元注解表示LockCommit
注解是可以被子类继承的。如果一个类被LockCommit
注解标记,那么它的所有子类也会隐式地拥有这个注解,除非它们被显式地用不同的注解标记了。
public @interface LockCommit
: 这一行声明了一个新的注解类型,名字为LockCommit
。public
关键字意味着这个注解可以被任何其他类或包访问。
String key() default "";
: 这是LockCommit
注解的一个成员,名称为key
,类型为String
,默认值为空字符串。这意味着当你使用LockCommit
注解时,你可以指定一个字符串值给key
属性,也可以选择不指定,此时key
将采用默认值,即空字符串。总结起来,
LockCommit
是一个可以在方法级别使用的注解,它携带一个可选的key
属性,这个属性用于存储字符串类型的值。由于它的保留策略是RUNTIME
,因此可以在运行时通过反射获取到这个注解以及它的key
属性值。这可能用于某些需要在运行时动态获取方法特定信息的场景,比如事务管理、缓存控制或者其他形式的锁机制。
03 编写一个拦截器
/**
* @Author:crispsea
* @Date :2024/6/20 - 06 - 20 - 15:47
*/
@Slf4j
@Component
@Aspect
public class NoRepeatSubmitAspect {
public static final Cache<String,Object> CACHES = CacheBuilder.newBuilder()
.maximumSize(50)
.expireAfterWrite(2, TimeUnit.SECONDS)
.build();
@Pointcut("@annotation(com.drpanda.csservice.domain.annotation.LockCommit)")
public void pointCut(){}
@Around("pointCut()")
public Object Lock(ProceedingJoinPoint joinPoint){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
LockCommit lockCommit = method.getAnnotation(LockCommit.class);
String key = lockCommit.key();
if(key!=null &&!"".equals(key)){
if(CACHES.getIfPresent(key)!=null){
assert response != null;
response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
}
CACHES.put(key,key);
}
Object object = null;
try {
object = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return object;
}
}
@Slf4j
@Component
@Aspect
@Slf4j
:这是一个Lombok提供的注解,用于自动注入日志对象。它会生成一个名为log
的日志记录器,方便进行日志记录。@Component
:Spring框架中的注解,表明这是一个Spring管理的Bean。@Aspect
:Spring AOP中的注解,表明这是一个切面类,用于定义横切关注点(如事务管理、日志记录等)。
这段代码主要是一个环绕通知(Around Advice),用于处理注解
@LockCommit
的方法,以防止重复提交。接下来我将逐步分解并详细解释这段代码。详细分解
缓存的定义:
public static final Cache<String,Object> CACHES = CacheBuilder.newBuilder() .maximumSize(50) .expireAfterWrite(2, TimeUnit.SECONDS) .build();
CopyInsert
- 使用 Guava 的
CacheBuilder
创建一个缓存CACHES
。maximumSize(50)
:缓存最多存储 50 个条目。expireAfterWrite(2, TimeUnit.SECONDS)
:每个条目在写入后 2 秒内有效,超过时间后会自动失效。定义切点:
@Pointcut("@annotation(com.drpanda.csservice.domain.annotation.LockCommit)") public void pointCut(){}
CopyInsert
- 使用
@Pointcut
注解定义一个切点,表示任何被LockCommit
注解的方法都会被这个切点匹配。环绕通知:
@Around("pointCut()") public Object Lock(ProceedingJoinPoint joinPoint) {
CopyInsert
@Around
注解表明这个方法是一个环绕通知,会在切点匹配的方法执行前后被调用。ProceedingJoinPoint
是一个可以控制目标方法执行的参数。获取 HTTP 响应:
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CopyInsert
- 从
RequestContextHolder
获取当前请求的HttpServletResponse
对象,以便在需要时修改响应状态。获取方法和注解信息:
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); LockCommit lockCommit = method.getAnnotation(LockCommit.class); String key = lockCommit.key();
CopyInsert
- 通过
joinPoint
获取被调用的方法的签名和具体方法。- 从方法中获取
LockCommit
注解,并提取出注解中的key
。检查缓存:
if (key != null && !"".equals(key)) { if (CACHES.getIfPresent(key) != null) { assert response != null; response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); return null; } CACHES.put(key, key);
CopyInsert
- 判断
key
是否有效,如果缓存中已经存在该key
,则表示该请求正在处理中。- 如果存在,设置 HTTP 响应状态为 503(服务不可用),然后返回 null,终止方法执行。
- 如果不存在,将该
key
放入缓存以标记为正在处理。执行目标方法:
try { Object object = joinPoint.proceed(); CACHES.invalidate(key); return object; } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(e); }
CopyInsert
- 调用
joinPoint.proceed()
执行目标方法。- 正常执行后,移除缓存中该
key
的条目。- 捕获任何异常,打印堆栈信息并抛出运行时异常。
没有锁定的处理:
try { return joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); throw new RuntimeException(e); }
CopyInsert
- 如果
key
是空的或无效,直接执行目标方法并处理异常。总结
这段代码的主要功能是在使用
@LockCommit
注解的方法上实现防止重复提交的机制。通过使用缓存来存储正在处理的请求标记,若在有效时间内再次请求相同的操作,则会返回 503 错误,避免重复处理。此代码适用于需要保护幂等性或避免重复操作的场景,如支付、提交表单等。
04 在controller对应的业务上添加注解
key最好唯一,且修改跟删除较多的地方可以考虑使用
05 前端回显
因为这里设置了全局异常拦截,前端直接配置就可以获取到接口返回的异常信息