Spring工程防止重复提交

  • Post author:
  • Post category:其他




解决思路

利用Redis记录请求的信息,一定时间内多个相同的请求只放行第一个。(代码在结尾)



实现过程



一、利用Interceptor拦截器获取请求的信息



1、获取接口映射地址

新建一个拦截器类

实现HandlerInterceptor接口

重写preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)方法

调用 request.getRequestURI() 获取接口映射的地址



2、获取请求参数

调用 request.getParameterMap() 获取请求参数

返回的类型为Map,利用工具类转换为字符串即可。



3、获取请求体

调用 request.getInputStream() 获取请求体

返回的类型为byte[],利用工具类转换为字符串即可。(由于Inputstream只能读取一次,在拦截器中获取RequestBody后会照成Controller中无法拿到RequestBody,这时我们需要自定义一个HttpServletRequest装饰类,使RequestBody可以重复获取。)

将请求的数据进行自定义拼接,作为key存入redis,这个key存在则说明这次为重复请求,进行拦截。



二、自定义HttpServletRequest装饰类,使RequestBody可重复获取



1、存放RequestBody

新建一个类,继承HttpServletRequestWrapper

添加一个类型为字节数组byte[]的body属性 用于存放请求体的流

在构造方法中直接获取RequestBody,并存放到body中

public ReHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.body = IOUtils.toByteArray(request.getReader());
}



2、取出RequestBody

重写 getInputStream() 和 getReader() 方法,手动返回body中的数据

   @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream stream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int available() throws IOException {
                return body.length;
            }

            @Override
            public int read() throws IOException {
                return stream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }



三、使用Filter过滤器,把拦截器中的Request转换为自定义的Request

新建一个拦截器类,实现Filter接口,并添加@Configuration注解

重写doFilter方法,把ServletRequest转换为自定义的HttpServletRequest装饰类,交给下一个过滤器

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 将ServletRequest转换为自定义的可重复获取body的Request
        ReHttpServletRequestWrapper request = new ReHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        filterChain.doFilter(request, servletResponse);
    }



四、将拦截器添加到WebMvcConfig中

新建一个配置类,实现WebMvcConfigurer接口,并添加@Configuration注解

注入第一步编写的拦截器

重写addInterceptors方法,将拦截器添加进去,并自定义需要拦截的路径。

    @Autowired
    private NoRepeatSubmitInterceptor noRepeatSubmitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(noRepeatSubmitInterceptor);
                // 需要拦截的地址
                .addPathPatterns("/api/**")
                // 拦截中排除的地址
                .excludePathPatterns("/api/test");
    }

重启服务后,实现禁止重复提交

在多线程的并发请求中,只有一个进入Controller,其他全部拦截。

测试结果



完整代码



拦截器


import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class NoRepeatSubmitInterceptor implements HandlerInterceptor {

    private static final long REPEAT_TIME = 1;
    private static final String REDIS_KEY = "submit:";
	
	//注入redis
    //@Autowired
    //private RedisClient redisClient;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ReHttpServletRequestWrapper reHttpServletRequestWrapper = (ReHttpServletRequestWrapper) request;
        StringBuilder nowParams = new StringBuilder();
        nowParams.append("Body:").append(reHttpServletRequestWrapper.getBodyString());
        nowParams.append("Params:").append(JSONUtil.toJsonStr(request.getParameterMap()));
        String uri = request.getRequestURI();
        //存入redis,并自行判断
        //Result<Boolean> result = redisClient.setIfAbsent(REDIS_KEY + uri + nowParams, nowParams.toString(), REPEAT_TIME, TimeUnit.SECONDS);
        //return result.getData();
		return true;
    }
    
}



HttpServletRequest包装类型


import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;

public class ReHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    public ReHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.body = IOUtils.toByteArray(request.getReader());
    }

    public String getBodyString() {
        return new String(body, StandardCharsets.UTF_8);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream stream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int available() throws IOException {
                return body.length;
            }

            @Override
            public int read() throws IOException {
                return stream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

}



过滤器


import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.*;

@Slf4j
@Configuration
public class NoRepeatSubmitFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 将ServletRequest转换为自定义的可重复获取body的Request
        ReHttpServletRequestWrapper request = new ReHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        filterChain.doFilter(request, servletResponse);
    }

}



WebMvcConfig


import one.project.common.interceptor.AuthorizationInterceptor;
import one.project.common.interceptor.NoRepeatSubmitInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private NoRepeatSubmitInterceptor noRepeatSubmitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(noRepeatSubmitInterceptor);
    }
}



版权声明:本文为bj791549729原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。