背景
重复提交一直是一个老生常谈的问题,在我们系统中也出现过同一用户同一个操作短时间多次重复提交的案例。我们系统中已经有一些处理重复提交的方法,比如在很多场景下,前端的按钮在提交之后后台返回之前做了按钮禁用处理,后台也加了版本控制(自定义的乐观锁version)等。我们是一个web项目,有些场景下并不能完全解决重复提交的问题。现引入另外一种实现方式,使用自定义注解+拦截器对加有注解的方法进行拦截,判断一定时间内是否有重复提交。
实现
- 自定义注解PreventRepeatSubmit,注解的Target:方法上
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventRepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交, 默认5s内不可重复提交
*/
int interval() default 5000;
/**
* 时间单位
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 提示消息
*/
String message() default "操作太频繁,请勿重复提交";
}
- 拦截器对有@PreventRepeatSubmit注解的方法进行拦截处理
@Component
public class PreventRepeatSubmitInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(PreventRepeatSubmitInterceptor.class);
private static final String REPEAT_SUBMIT_KEY = "preventRepeatSubmit:";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取注册方法
Method method = handlerMethod.getMethod();
// 获取方法上的注解@PreventRepeatSubmit
PreventRepeatSubmit annotation = method.getAnnotation(PreventRepeatSubmit.class);
// 获取请求的地址
String url = request.getRequestURI();
// 判断注解是否存在
if (annotation != null) {
// 获取请求的参数
String params = getParamStr(request);
// 获取token
String token = request.getHeader("xxx-token");
// =======================思路==========================
// 1. 通过REPEAT_SUBMIT_KEY + 请求的url + token + 请求的参数列表作为key值
// 2. 判断key值是否在redis中存在,如果存在代表重复提交,系统抛出重复提示信息
// 3. 如果key值在redis中不存在,则写入到redis中,并且将间隔时间设置为redis失效
时间
// 缓存到redis的key, 参数进行MD5加密,保证数据安全性
String cacheKey = REPEAT_SUBMIT_KEY + url + token + ":" + DigestUtils.md5DigestAsHex(params.getBytes());
String key = RedisUtil.stringGet(cacheKey);
if (key == null) {
RedisUtil.stringSet(cacheKey, "", annotation.interval(), annotation.timeUnit());
} else {
logger.warn("页面重复提交,url:{}", url );
String message = annotation.message();
throw new RuntimeException(message);
}
}
}
return true;
}
private String getParamStr(HttpServletRequest request) throws Exception{
String curParams = StringUtils.EMPTY;
ParameterRequestWrapper wrapper = (ParameterRequestWrapper) request;
// 获取post请求的参数
curParams = wrapper.getBodyString();
if (StringUtils.isEmpty(curParams)){
// 获取get请求的参数
curParams = JSON.toJSONString(wrapper.getParameterMap());
}
return curParams;
}
}
- 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private PreventRepeatSubmitInterceptor preventRepeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(preventRepeatSubmitInterceptor);
}
}
- 使用注解
@PreventRepeatSubmit(interval = 3000, timeUnit = TimeUnit.MILLISECONDS, message = "操作太频繁,请稍后再试") @RequestMapping("/updateUser") public Response updateUser(@RequestBody User user) throws Exception{ User updatedUser = userService.updateUser(user); return new Response(updatedUser); }