原理:
常见的场景端页面多次点击提交按钮,通常是前端通过点击一次后使按钮disable进行处理,后端同样也需要进行限制。若依使用了注解+拦截器的方式,这里其实也可以用AOP。在缓存中(若依的缓存就是使用redis)记录每个客户端的请求方法和参数,在redis中设置超时时间。如果在超时时间内进行了第二次请求且参数都一致,拦截器进行拦截抛出异常不进行真正的处理。思路其实和限流类似,只是这里多了对请求参数的处理,并且没有采用AOP而是用了拦截器去实现。
1)添加过滤器,处理请求参数不能多次读的问题。
自定义了一个HttpServletRequest的包装器,通过在变量中保存流中读取的内容,解决了不能多次读入请求参数的问题。然后通过添加过滤器在dofilter中替换了这个增强功能后的request。包装器代码:
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding(Constants.UTF8);
response.setCharacterEncoding(Constants.UTF8);
// 把reques的参数读入到byte数组中,就可以多次使用
body= HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream bis = 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 read() throws IOException {
return bis.read();
}
};
}
}
把这个包装器通过过滤器加入到请求处理中去。
public class RepeatableFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
/**
* 参数如果是json类型,需要HttpServletRequest包装器进行加强,多次读取参数
*/
if(request instanceof HttpServletRequest
&& request.getContentType().startsWith(MediaType.APPLICATION_JSON_VALUE)){
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request,response);
}
if(requestWrapper == null){
filterChain.doFilter(request,response);
}else{
filterChain.doFilter(requestWrapper, response);
}
}
}
最后要记得把这个过滤器注册到spring环境里。
@Component
public class FilterConfig {
@Bean
public FilterRegistrationBean repeatableFilterRegistration(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}
springboot通过拦截器获取参数有两种方式,一种通过request.getParameter获取Get方式传递的参数,另外一种是通过request.getInputStream或reques.getReader获取通过POST/PUT/DELETE/PATCH传递的参数。@PathVariable注解是REST风格url获取参数的方式,只能用在GET请求类型,通过getParameter获取参数;@RequestParam注解支持GET和POST/PUT/DELETE/PATCH方式,Get方式通过getParameter获取参数和post方式通过getInputStream或getReader获取参数;@RequestBody注解支持POST/PUT/DELETE/PATCH,可以通过getInputStream和getReader获取参数。通过getInputStream或getReader在拦截器中获取会导致控制器拿到的参数为空,这是因为流读取一次之后流的标志位已经发生了变化,无法多次读取参数,所以通过装饰者模式,增强了HttpServletRequest,把流中的数据读取后暂存在byte[]中,可以实现多次读取的需求。
2)添加拦截器
拦截到自定义注解进行处理 。通过AbstractRepeatSubmitInterceptor拦截请求,进行重复提交的判断和处理。
3)测试
controller中建立添加注解的测试方法:
@PostMapping("/repeatTest")
@RepeatSubmit(interval = 100000, message = "重复提交提示!!!")
public String repeatTest(@RequestBody String json) {
return json;
}