场景描述

在开发过程中有时会出现网络延迟或多次点击提交按钮产生一个请求发送多次的情况,这类情况实际上只有第一个请求是客户端想发送的后面的请求对客户端并没有意义。例如客户端发送了5个保存的请求,实际客户端只想保存一条记录但服务端会保存五条,这就会导致重复数据,并且这些数据会对系统造成不必要的影响。为了防止此类情况发生提供以下解决办法。

  1. 前端解决
    1.1 操作后在数据未返回之前将操作按钮设置为不可点击
    1.2 操作后再前端进行锁屏
  2. 后端解决(本文推荐)
    2.1 先自定义防止重复的注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventRepeat {
}

2.2 自定义拦截器

import com.engraver.framework.exception.LocalBusinessException;
import com.engraver.framework.annotation.PreventRepeat;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 相同url和数据拦截器 为了防止重复提交等操作
 * 继承拦截器适配器
 * @author 高健
 * @date 2019年2月26日14:07:08
 */
public class SameUrlDataInterceptor extends HandlerInterceptorAdapter {

    /**
     * 覆盖父类的preHandle方法
     * 预处理回调方法,实现处理器的预处理,验证是否为重复提交,第三个参数为响应的处理器,自定义Controller
     * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. 判断handler参数是否为HandlerMethod类的实例
        if (handler instanceof HandlerMethod) {

            // 2. 获取方法注解查看方式是否有PreventRepeat注解
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            PreventRepeat annotation = method.getAnnotation(PreventRepeat.class);
            if (annotation != null) {

                // 3. 调用重复数据验证方法
                boolean result = repeatDataValidator(request);
                if(result){
                    return false;
                }
                else{
                    return true;
                }

            }else{
                return true;
            }
        } else {

            // 4. 如果参数不是HandlerMethod类的实例则调用父类的preHandle方法
            return super.preHandle(request, response, handler);
        }
    }

    /**
     * 验证同一个url数据是否相同提交,相同返回true
     * @param httpServletRequest
     * @return
     */
    public boolean repeatDataValidator(HttpServletRequest httpServletRequest) throws Exception{

        try {

            // 1. 将请求参数转换为json字符串 需要在pom内引用jackson-databind
            ObjectMapper objectMapper = new ObjectMapper();
            String params = objectMapper.writeValueAsString(httpServletRequest.getParameterMap());

            // 2. 获取当前请求的url地址 并以url为key 参数为值存在map内
            String url=httpServletRequest.getRequestURI();
            Map<String,String> map=new HashMap(4);
            map.put(url, params);
            String nowUrlParams=map.toString();

            // 3. 获取session中上一次请求存储的url和参数字符串
            Object preUrlParams=httpServletRequest.getSession().getAttribute("oldUrlParams");

            // 4. 如果上一个数据为null,表示还没有访问页面 将当前方位的url和请求参数存储到session中
            if(preUrlParams == null) {
                httpServletRequest.getSession().setAttribute("oldUrlParams", nowUrlParams);
                return false;
            } else {

                // 5. 判断上一次访问的url和参数与本次是否相同 如相同则表示重复数据
                if(preUrlParams.toString().equals(nowUrlParams))
                {
                    return true;
                }
                else
                {
                    httpServletRequest.getSession().setAttribute("oldUrlParams", nowUrlParams);
                    return false;
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
            // 此处是我自定义异常
            throw new LocalBusinessException("验证是否为重复请求时出错了!");
        }
    }

2.3 在spring mvc 配置文件中加入如下配置

<!-- 自定义相同url和数据的拦截器 拦截所有的url -->
<mvc:interceptors>
      <mvc:interceptor>
          <!-- 拦截url -->
          <mvc:mapping path="/**"/>
          <!-- 自定义拦截器类 -->
          <bean class="com.engraver.framework.interceptor.SameUrlDataInterceptor"/>
      </mvc:interceptor>
</mvc:interceptors>

2.4 使用方法
在controller类的方法上增加@PreventRepeat注解

/**
     * 执行登录
     * 增加 @PreventRepeat 注解是为了防止重复登录提交
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login")
    @ResponseBody
    @PreventRepeat
    public WebResponse login(
            HttpServletRequest request,
            @RequestParam(value = "username", required = false) String username,
            @RequestParam(value = "password", required = false) String password,
            @RequestParam(value = "validateCode", required = false) String validateCode) throws Exception{

           // TODO 登录实现
}