Spring工程防止重复提交
解决思路
利用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);
}
}