背景
在实际开发过程中,防重复提交的操作很常见。有细分配置针对某一些路径进行拦截,也有基于注解去实现的指定方法拦截的。
分析
实现原理
实现防重复提交,我们很容易想到就是用过滤器或者拦截器来实现。
使用拦截器就是继承HandlerInterceptorAdapter
类,实现preHandle()
方法;
使用过滤器就是实现OncePerRequestFilter
接口,在doFilterInternal()
完成对应的防重复提交操作。
OncePerRequestFilter接口详解
在Spring Web应用程序中,过滤器(Filter)也是一种拦截HTTP请求和响应的机制,可以对它们进行处理或修改,从而增强或限制应用程序的功能。OncePerRequestFilter类是Spring提供的一个抽象类,继承自javax.servlet.Filter类,并实现了Spring自己的过滤器接口OncePerRequestFilter,它的目的是确保过滤器只会在每个请求中被执行一次,从而避免重复执行过滤器逻辑所带来的问题,如重复添加响应头信息等。
OncePerRequestFilter类中有一个doFilterInternal()方法,用于实现过滤器的逻辑,该方法只会在第一次请求时被调用,之后不再执行,确保了过滤器只会在每个请求中被执行一次。
实际场景考虑
使用过滤器的话,会对所有的请求都进行防重复提交。但对于一些查询接口来说,并不需要防重复提交。那么怎样在指定的接口需要使用防重复提交拦截呢?答案就是用注解。
实现步骤
1.定义注解@DuplicateSubmission
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 DuplicateSubmission {
}
2.DuplicateSubmissionFilter
实现防重复提交
方法一:基于过滤器与session
public class DuplicateSubmissionFilter extends OncePerRequestFilter {
private final Logger LOGGER = LoggerFactory.getLogger(DuplicateSubmissionFilter.class);
@Value("app.duplicateSubmission.time")
/** 两次访问间隔时间 单位:毫秒 */
private long intervalTime;
@Autowired
private HttpSession session;
@Autowired
private HandlerMapping handlerMapping;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
HandlerMethod handlerMethod = getHandlerMethod(request);
if (handlerMethod != null && handlerMethod.getMethodAnnotation(DuplicateSubmission.class) != null) {
// 这里的token不一定是要用户标识,如果是设备之类也行,能有唯一性就好
String token = request.getHeader("token");
// 这里存到session中,也可以用redis改造
if (token == null) {
LOGGER.warn("token为空!");
}
String key = token + request.getRequestURI();
long nowTime = System.currentTimeMillis();
Object sessionObj = session.getAttribute(key);
if (sessionObj != null) {
long lastTime = (long) sessionObj;
session.setAttribute(key, nowTime);
// 两次访问的时间小于规定的间隔时间
if (intervalTime > (nowTime - lastTime)) {
LOGGER.warn("重复提交!");
return;
}
}
}
filterChain.doFilter(request, response);
}
private HandlerMethod getHandlerMethod(HttpServletRequest request) throws NoSuchMethodException {
HandlerExecutionChain handlerChain = null;
try {
handlerChain = handlerMapping.getHandler(request);
} catch (Exception e) {
LOGGER.error("Failed to get HandlerExecutionChain.", e);
}
if (handlerChain == null) {
return null;
}
Object handler = handlerChain.getHandler();
if (!(handler instanceof HandlerMethod)) {
return null;
}
return (HandlerMethod) handler;
}
}
但其实这个方案还需要考虑一个场景:如果设置的防重复提交时间间隔小,用户体验不会有什么奇怪。如果设置了1分钟以上,那我们要考虑完善这个方案,防重复提交还有一个重要的判断依据,就是参数相同。**当时间小于间隔时间,且参数相同时,认定为重复提交。**这一步也没什么复杂,就只是建一个map,把请求时间和参数放进map,再保存到 session中。
方式二
基于拦截器与redis实现,使用拦截器记得要在你的WebMvcConfigurer
实现类上注册!
@Component
@Slf4j
public class DuplicateSubmissionInterceptor implements HandlerInterceptor {
@Value("app.duplicateSubmission.time")
/** 两次访问间隔时间 单位:毫秒 */
private long intervalTime;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
DuplicateSubmission annotation = method.getAnnotation(DuplicateSubmission.class);
// 使用了注解
if (annotation != null) {
// 获取key值
String key = token + request.getRequestURI();
// 直接用redis的setIfAbsent
boolean firstRequest = redisTemplate.opsForValue().setIfAbsent(key, "flag", intervalTime, TimeUnit.SECONDS);
// 如果设置不成功,那就是重复提交
if (!firstRequest) {
log.warn("重复提交");
// 通常来说这里还有抛个全局处理异常
return;
}
}
}
return true;
}
}
同样的,如果设置的重复提交过长,则需要把请求参数放到redis的value值中(上面只是用了"flag"作为一个假的值),对比请求参数是否一致。
使用方式
使用方法很简单,只需要在需要进行防重复提交的方法上加上一个注解即可
@PostMapping("/submit")
@DuplicateSubmission
public String submitForm() {
// 处理表单提交请求
// ...
return "result";
}