思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
浅谈SpringMVC
中的拦截器在防止请求的重复提交中的应用。
注意:本文只是抛砖引玉式的简单谈一谈SpringMVC
中拦截器在防止请求重复提交中的应用,因此文中提供案例所提供代码可能很简陋,所以请勿直接应用于项目!
前言
在分析之前,我们先来看下什么是请求的重复提交。
请求重复提交
通常是指用户在操作时多次多次发送相同请求,进而导致相同请求被服务器处理多次。更进一步,当请求不断重复提交后,还可能导致如下问题:
- 数据一致性问题: 如果一个表单提交包含对数据库或后端资源的修改操作,重复提交可能导致数据不一致。例如,如果用户多次提交相同的订单或支付请求,可能会导致多次扣款或重复生成订单。
- 资源浪费: 处理重复请求可能会浪费服务器资源和带宽。服务器需要处理相同的请求多次,而客户端需要多次下载和显示相同的响应。
- 系统性能问题: 大规模的请求重复提交可能会对服务器性能产生负面影响,尤其是在高负载情况下。服务器可能会因处理重复请求而变得不稳定。
因此,为了防止请求的重复提交,应采取适当的措施来避免请求被重复提交。
防止请求重复提交的常见手段
通过之前的分析不难发现,请求的重复的本质就是一个本应该发送一次的请求,因为操作不当
而导致发送了多次,进而导致了请求的重复提交。在Web
业务开发中,为了应对这一问题通常可以采取如下手段来进行预防:
- Token验证: 在服务器端生成一个唯一的令牌(Token),将其包含在响应中并存储在前端(通常在表单中的隐藏字段或者请求头中)。在接收到请求时,服务器验证令牌的唯一性,如果令牌已被使用过,拒绝重复请求。
- 过期时间: 在前端和后端都可以设置一个请求的有效时间戳。当请求被发送到服务器时,服务器可以检查请求的时间戳,如果请求时间戳过期,拒绝处理请求。
- 数据库记录: 在服务器端,可以将已经处理的请求记录在数据库中,通过检查请求是否已存在来防止重复请求。这种方法适用于处理重复数据写入数据库的情况。
其实无论采用什么方法,其核心的本质就是一段时间内仅能允许请求被提交一次。而为了达到这一目的最好的办法就是为请求
添加一个唯一标示。因此无论是Token
还是数据库记录,其核心都在于借助外界
的力量来为无状态
的http
请求构建状态。
总之,防止请求重复提交的关键点在于确保请求的唯一性。当理解了这一点后,其实也就把握住了问题的关键,这样后续即使接触到其他防止请求重复处理的方式,也能快速上手,所以在学习过程中一定要找到问题的本质逻辑,不要将太多精力放在技巧上,更应关注事物的底层了逻辑。
说了这么多,我们来看看如何通过SpringMVC
的拦截器如何在防止请求重复提交中发挥作用。
利用拦截器避免请求重复提交
java
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashSet;
import java.util.Set;
public class DuplicateSubmitInterceptor
implements HandlerInterceptor {
/**
* 存放Token的集合
**/
private Set<String> tokens = new HashSet<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("token"); // 从请求参数中获取token
if (token != null) {
if (tokens.contains(token)) {
// 如果token已经存在,说明是重复提交,拦截请求
return false;
}
// 否则,将token添加到集合中
tokens.add(token);
}
return true;
}
}
在上述示例中,我们创建了一个DuplicateSubmitInterceptor
拦截器,它使用tokens
集合来保存已经提交的token
。如果当前Token
已经在集合中,则表明当前用户已经提交相关请求,后续请求则不会到达Controller
层进行单独处理。
(注:如果你对SpringMVC
中拦截器的工作原理有所疑惑可参考笔者之前的SpringMVC流程分析(五):HandlerInterceptor组件分析与责任链模式在SpringMVC中的应用 进行了解!)
上述代码其实已经可以完成任务了,但是不知道是否考虑过这样的一个问题。即前台当请求过多时DuplicateSubmitInterceptor
中的tokens
会缓存大量信息,这非常占用程序内存大小。同时其中的好多token
信息其实是无效的,一个比较不错的方法就是定时清除!
为了实现定时清除,在SpringBoot
的应用中我们可以通过@Scheduled
注解来创建一个定时任务。引入@Scheduled
注解后,代码变为如下形式:
java
@Component
public class DuplicateSubmitInterceptor implements HandlerInterceptor {
private Set<String> tokens = new HashSet<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("token");
if (token != null) {
if (tokens.contains(token)) {
// 如果token已经存在,说明是重复提交,拦截请求
return false;
}
// 否则,将token添加到集合中并设置有效期
tokens.add(token);
}
return true;
}
@Scheduled(fixedRate = 1800000) // 每30分钟执行一次
public void cleanUpExpiredTokens() {
long currentTime = System.currentTimeMillis();
tokens.removeIf(token -> isTokenExpired(token, currentTime));
}
private boolean isTokenExpired(String token, long currentTime) {
// 根据Token的创建时间和过期时间判断Token是否过期
// 这里假设Token的有效期是30分钟
// 实际情况中,你可以根据需求自定义Token的有效期判断逻辑
// 这里使用了毫秒作为时间单位
long tokenCreationTime = Long.parseLong(token); // 假设Token中存储了创建时间
long tokenExpirationTime = tokenCreationTime + TimeUnit.MINUTES.toMillis(30); // 30分钟有效期
return currentTime > tokenExpirationTime;
}
}
在上述代码中,我们创建了一个cleanUpExpiredTokens
方法,它会每隔30
分钟执行一次,用于清理过期的Token
。进一步,isTokenExpired
方法用于判断Token
是否过期,过期的Token
将从tokens
集合中移除。
总结
在如下图所示的MVC
架构中,只要你愿意你可以将防止请求重复提交的校验逻辑写在控制层
或者服务层
中,也不一定非常通过SpringMVC
中的拦截器实现,但这样做的弊端就是控制层
和服务层
的功能划分变的臃肿,这其实与MVC
分层划分的思想是背道而驰的。
再回到今天我们所讨论的话题,其实你会发现,我们在通过拦截器
实现时,其本质思想就是为Http
请求创建唯一标示
,然后进行唯一性校验。为此我们在拦截器
中通过一个set
集合来存储标示
,并进行判断。进一步,其可能诱发的问题就是内存溢出。
为了解决这一问题,我们引入了定时清除机制来进行代码的优化。不知道你有没有发现这样的做法其实与Redis
的处理思想很类似?事实上,很多知识间都是连通的,其永远不是孤立的。希望文章对你学习有所启发。
最后,如果觉得文章对你有帮助的话,不妨点赞加关注,不错过笔者
之后的每一次更新!