如果没有防重复提交,当用户在做一个新增操作时,多次点击新增按钮,那么会在数据库生成多条一模一样的数据。前端处理的方式就是,当用户点击新增后,禁用按钮,直到服务端响应成功。有句话说得好,作为一个服务端开发人员,不能相信客户端的任何输入。所以,一个健壮的系统,不仅要有前端校验,服务端校验更是必不可少。
实现思路
1、当客户端发起第一次请求,记录下该次请求。
2、当客户端发起第二次请求的时候,校验上次请求是否在指定的限制重复请求时间内。如果在,抛出指定的异常;如果不在,则放行请求。
引入jar包
说明:本示例基于springboot
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
该jar包主要作用是支持aop编程
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<!--使用最新版本即可-->
<version>last-version</version>
</dependency>
该jar包主要用来作客户端访问标识的缓存工具。如果要用于集群系统,使用redis
自定义注解
针对于读操作的接口,不管如何请求,都不会对数据产生任何改变。所以,在一个系统中,不可能一刀切的对所有接口进行防重复提交,这里就自定义一个注解,用于标识需要防重复提交的接口。
// 指定当前注解只能用于方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SubmitLock {
/**
* 对接口指定唯一标识
* */
String key();
}
利用aop编写拦截
@Aspect
@Configuration
public class SubmitLockInterceptor {
// 声明缓存服务:利用本地缓存,省去网络传输
private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
// 最大缓存 1000个
.maximumSize(1000)
// 设置缓存5 秒钟过期:即5s内禁止重复提交
.expireAfterWrite(5, TimeUnit.SECONDS).build();
/**
* 切入目标:公共方法且带有@SubmitLock注解
*/
@Around("execution(public * *(..)) && @annotation(com.zepal.lock.annotation.SubmitLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 获取当前执行方法
Method method = signature.getMethod();
// 获取当前方法的@SubmitLock注解
SubmitLock submitLock = method.getAnnotation(SubmitLock.class);
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 获取当前登录用户的唯一标识(用户标识的参数名可能不尽相同)
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
token = request.getParameter("token");
}
if (!StringUtils.hasText(token)) {
// 客户端未传递token参数
throw new RuntimeException("参数:token不能为空");
}
// 利用token参数生成唯一key
String key = submitLock.key() + token;
if (CACHES.getIfPresent(key) != null) {
// 利用double check lock,保证线程安全
synchronized (this) {
if (CACHES.getIfPresent(key) != null) {
// 缓存中存在相应的token
throw new RuntimeException("请勿重复请求");
}
}
}
// 如果是第一次请求,就将当前请求唯一标识压入缓存中
CACHES.put(key, key);
try {
// 回调目标方法(即放行请求后执行原有的业务逻辑)
return pjp.proceed();
} catch (Throwable throwable) {
// 被aop代理的目标方法发生了异常(当然是程序出bug了)
throw new RuntimeException("系统异常:" + throwable.getMessage(), throwable);
}
}
}
测试
@Controller
public class TestController {
// 指定@SubmitLock的标识要保证整个系统唯一,否则会导致多个不同功能的接口被防重复拦截
@SubmitLock(key = "submitLockTest")
@PostMapping("/submitLockTest")
@ResponseBody
public String submitLockTest(String token) {
return "success";
}
}
5s内重复对目标接口发起请求,会响应以下异常(项目中,这里可以使用全局异常拦截将异常信息处理得更加优雅)