Spring Boot自定义注解实现防表单重复提交
1. 为什么我们需要防止表单重复提交在Web开发中表单重复提交是一个常见但容易被忽视的问题。想象一下这样的场景用户在电商网站点击提交订单按钮后由于网络延迟或页面响应慢用户可能会多次点击同一个按钮导致同一个订单被重复创建。这不仅会造成数据混乱还可能引发财务问题。表单重复提交通常发生在以下几种情况用户快速多次点击提交按钮页面提交后刷新提交后点击浏览器后退按钮再次提交网络延迟导致用户误以为提交失败而重试在传统的Java Web应用中我们可能会在服务端使用Token机制来防止重复提交但在Spring Boot中我们可以通过更优雅的方式——自定义注解来实现这一功能。2. 自定义注解的核心设计思路2.1 注解的基本概念在Java中注解Annotation是一种元数据形式它提供了一种向代码添加元信息的方式。Spring框架大量使用了注解来简化配置和实现各种功能。自定义注解的核心优势在于声明式编程通过简单的注解标记就能实现复杂功能代码解耦业务逻辑与防重提交逻辑分离可重用性一次开发多处使用2.2 防重提交注解的设计我们需要设计的防重提交注解NoRepeatSubmit应该包含以下特性可以标记在Controller方法上能够设置锁的过期时间支持自定义错误消息能够区分不同用户的请求注解的初步设计如下Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface NoRepeatSubmit { /** * 锁的过期时间默认5秒 */ int lockTime() default 5; /** * 自定义错误消息 */ String message() default 请勿重复提交; }3. 实现防重提交的核心组件3.1 分布式锁的选择与实现在分布式环境下我们需要一个可靠的锁机制来防止重复提交。Redis是一个理想的选择因为它性能高支持原子操作可以设置过期时间在分布式环境中表现良好我们使用Spring Data Redis来实现Redis锁Component public class RedisLock { Autowired private StringRedisTemplate stringRedisTemplate; /** * 尝试获取锁 * param key 锁的key * param value 锁的value * param expire 过期时间(秒) * return 是否获取成功 */ public boolean tryLock(String key, String value, int expire) { Boolean result stringRedisTemplate.opsForValue() .setIfAbsent(key, value, expire, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); } /** * 释放锁 * param key 锁的key * param value 锁的value */ public void unlock(String key, String value) { String currentValue stringRedisTemplate.opsForValue().get(key); if (currentValue ! null currentValue.equals(value)) { stringRedisTemplate.delete(key); } } }3.2 拦截器的实现我们需要一个拦截器来拦截带有NoRepeatSubmit注解的方法Component public class NoRepeatSubmitInterceptor implements HandlerInterceptor { Autowired private RedisLock redisLock; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; NoRepeatSubmit noRepeatSubmit handlerMethod.getMethodAnnotation(NoRepeatSubmit.class); if (noRepeatSubmit ! null) { String userId getUserId(request); // 获取用户ID的方法 String requestURI request.getRequestURI(); String key no_repeat_submit: userId : requestURI; String value UUID.randomUUID().toString(); if (!redisLock.tryLock(key, value, noRepeatSubmit.lockTime())) { throw new RuntimeException(noRepeatSubmit.message()); } request.setAttribute(no_repeat_submit_key, key); request.setAttribute(no_repeat_submit_value, value); } } return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; NoRepeatSubmit noRepeatSubmit handlerMethod.getMethodAnnotation(NoRepeatSubmit.class); if (noRepeatSubmit ! null) { String key (String) request.getAttribute(no_repeat_submit_key); String value (String) request.getAttribute(no_repeat_submit_value); if (key ! null value ! null) { redisLock.unlock(key, value); } } } } private String getUserId(HttpServletRequest request) { // 实际项目中可以从session或token中获取用户ID // 这里简化处理返回固定值 return user1; } }3.3 注册拦截器为了让拦截器生效我们需要在Spring Boot配置中注册它Configuration public class WebConfig implements WebMvcConfigurer { Autowired private NoRepeatSubmitInterceptor noRepeatSubmitInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(noRepeatSubmitInterceptor) .addPathPatterns(/**); } }4. 实际应用与测试4.1 在Controller中使用注解现在我们可以在任何需要防止重复提交的Controller方法上使用NoRepeatSubmit注解RestController RequestMapping(/order) public class OrderController { PostMapping(/create) NoRepeatSubmit(lockTime 10, message 订单正在处理中请勿重复提交) public ResponseEntityString createOrder(RequestBody OrderDTO orderDTO) { // 模拟业务处理时间 try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return ResponseEntity.ok(订单创建成功); } }4.2 测试防重提交功能我们可以使用Postman或curl命令来测试这个功能第一次发送请求成功创建订单在10秒内我们设置的lockTime再次发送相同请求会收到订单正在处理中请勿重复提交的错误10秒后再次发送请求可以成功创建新订单4.3 前端配合实现虽然服务端已经实现了防重提交但为了更好的用户体验前端也应该做一些处理提交按钮点击后禁用显示加载状态请求完成后才恢复按钮状态示例前端代码使用jQuery$(#submitBtn).click(function() { var $btn $(this); $btn.prop(disabled, true); $btn.text(处理中...); $.ajax({ url: /order/create, method: POST, data: JSON.stringify(orderData), contentType: application/json, success: function(response) { alert(订单创建成功); }, error: function(xhr) { alert(xhr.responseText); }, complete: function() { $btn.prop(disabled, false); $btn.text(提交订单); } }); });5. 进阶优化与注意事项5.1 锁粒度的优化我们当前的实现是以用户ID请求URI作为锁的key这在大多数情况下已经足够。但在某些特殊场景下可能需要更细粒度的控制。例如对于同一个用户对同一个商品多次下单的情况可以在key中加入商品IDString productId orderDTO.getProductId(); String key no_repeat_submit: userId : productId;对于需要区分参数的场景可以计算参数的MD5作为key的一部分String paramsJson objectMapper.writeValueAsString(orderDTO); String paramsHash DigestUtils.md5DigestAsHex(paramsJson.getBytes()); String key no_repeat_submit: userId : requestURI : paramsHash;5.2 异常处理与事务回滚在实际项目中我们需要考虑异常情况下的处理如果业务逻辑抛出异常应该确保锁被正确释放PostMapping(/create) NoRepeatSubmit public ResponseEntityString createOrder(RequestBody OrderDTO orderDTO) { try { // 业务逻辑 return ResponseEntity.ok(成功); } catch (Exception e) { // 记录日志等 throw e; // 抛出异常让拦截器处理锁释放 } }如果使用数据库事务需要注意锁的释放时机Transactional PostMapping(/create) NoRepeatSubmit public ResponseEntityString createOrder(RequestBody OrderDTO orderDTO) { // 业务逻辑 return ResponseEntity.ok(成功); }在这种情况下拦截器的afterCompletion方法会在事务提交后执行这是合理的。但如果希望在事务回滚时也释放锁可以使用TransactionalEventListener监听事务事件。5.3 性能考虑在高并发场景下防重提交机制可能会成为性能瓶颈。我们可以考虑以下优化使用本地缓存作为第一层防护Redis作为第二层防护public boolean tryLock(String key, String value, int expire) { // 先检查本地缓存 if (localCache.getIfPresent(key) ! null) { return false; } // 再检查Redis Boolean result stringRedisTemplate.opsForValue() .setIfAbsent(key, value, expire, TimeUnit.SECONDS); if (Boolean.TRUE.equals(result)) { localCache.put(key, value); } return Boolean.TRUE.equals(result); }对于特别高频的接口可以考虑使用更轻量级的锁实现如基于内存的RateLimiter。5.4 测试注意事项在测试防重提交功能时需要注意以下几点并发测试使用JMeter等工具模拟多用户并发提交网络延迟测试模拟慢速网络环境下的多次提交分布式测试在多个服务实例上测试锁的有效性异常测试测试在服务重启、Redis宕机等情况下的行为6. 替代方案比较除了自定义注解的方式还有其他几种常见的防重提交方案6.1 前端Token方案原理服务端生成唯一Token并存储在Session中页面加载时将Token作为隐藏字段提交时带上Token服务端验证后删除Token优点实现简单不依赖外部存储缺点无法防止恶意绕过前端的重复提交在分布式环境下需要额外处理Session共享6.2 数据库唯一索引原理为业务表添加唯一索引重复提交时会抛出唯一约束异常优点实现简单绝对可靠缺点只适用于有唯一业务键的场景数据库压力大不友好的错误提示6.3 请求指纹方案原理计算请求参数用户标识的指纹短时间内相同指纹的请求视为重复优点可以防止参数相同的重复提交粒度更细缺点计算指纹有性能开销对于大请求体不友好相比之下我们的自定义注解方案结合了Redis分布式锁的优势既保证了分布式环境下的可靠性又通过注解实现了声明式编程是较为理想的解决方案。

相关新闻

2026 AI 编程工具实测横评:Cursor、Windsurf、Copilot 谁能真正提升开发效率?

2026 AI 编程工具实测横评:Cursor、Windsurf、Copilot 谁能真正提升开发效率?

2026年,AI Coding 已经从“玩具”变成了开发者的“核心生产力工具”。但面对市面上琳琅满目的 AI 编程助手,究竟哪款才是你的最佳搭档?本文基于我过去半年的真实高强度开发记录,从代码补全、上下文理解、Token 经济学三个维度&…

2026/7/23 4:53:36 阅读更多 →
WebRTC+Vue3+Electron构建跨平台远程桌面系统实战

WebRTC+Vue3+Electron构建跨平台远程桌面系统实战

1. 项目背景与技术选型这个开源远程桌面项目采用WebRTCVue3ElectronNodejs技术栈实现,累计获得100GitHub星标。项目名称为BilldDesk,实现了类似ToDesk、向日葵等商业远程桌面软件的核心功能。作为全栈工程师,我选择这套技术栈主要基于以下几个…

2026/7/24 3:43:32 阅读更多 →
Next.js 14技术革新与开发者实践指南

Next.js 14技术革新与开发者实践指南

1. Next.js 14技术革新与社区争议解析当Vercel在Next.js Conf上宣布Next.js 14发布时,官方将其描述为"最专注的版本更新"。但有趣的是,开发者社区却出现了两极分化的反应——有人称赞其性能突破,也有人戏称这是"最会营销的框架…

2026/7/23 6:14:56 阅读更多 →

最新新闻

Linux C++事件驱动编程:从回调机制到Reactor模式实现

Linux C++事件驱动编程:从回调机制到Reactor模式实现

1. 项目概述:为什么要在Linux下用C搞事件驱动?如果你在Linux上写过C的网络服务或者GUI程序,大概率会碰到一个经典难题:如何高效地处理来自多个来源的输入?比如,一个服务器要同时监听成百上千个客户端的连接…

2026/7/24 5:14:42 阅读更多 →
C++ STL deque内存管理:从分块原理到实战优化

C++ STL deque内存管理:从分块原理到实战优化

1. 项目概述:为什么需要深挖deque的内存管理?在C的标准模板库(STL)里,deque(双端队列)是个既熟悉又陌生的容器。很多开发者知道它支持头尾高效插入删除,也知道它不像vector那样在尾部…

2026/7/24 5:14:42 阅读更多 →
面向杭州商会的技术选型实践:私域管理系统的架构验证与能力压测路径

面向杭州商会的技术选型实践:私域管理系统的架构验证与能力压测路径

针对杭州地区商会组织的数字化升级需求,技术团队需跳出营销话术,聚焦可编码验证的能力边界。核心验证目标包括:多层级组织建模支持度、非雇员角色权限控制粒度、非结构化内容存储与检索能力、标准化数据出口完备性。建议采用分阶段技术验证流…

2026/7/24 5:14:42 阅读更多 →
上虞想要咖啡、甜点、中餐一站实现?这5家融合店不妨了解下

上虞想要咖啡、甜点、中餐一站实现?这5家融合店不妨了解下

在忙碌的生活中,我们常常渴望能有一个地方,既能品尝到香浓的咖啡,又能享用到精致的甜点,还能有美味的中餐可供选择,实现一站式的美食体验。而在上虞,就有这样几家宝藏融合店,今天就来给大家推荐…

2026/7/24 5:14:42 阅读更多 →
任务失败推送到飞书

任务失败推送到飞书

import oracledb import requests import json# 配置区 请自行修改 db_host "192.168.1.100" db_port 1521 db_service "" db_user "" db_pwd ""#飞书机器人Webhook 地址 FEISHU_WEBHOOK "https://" # # 扩展后的SQ…

2026/7/24 5:14:42 阅读更多 →
GitLab 19.2 发布:AI 助力自动修复依赖项,多功能突破代码审查与安全瓶颈

GitLab 19.2 发布:AI 助力自动修复依赖项,多功能突破代码审查与安全瓶颈

GitLab 19.2:多维度升级的 DevSecOps 平台GitLab 宣布发布 DevSecOps 平台的更新版本 GitLab 19.2,带来了一系列令人瞩目的新功能。其中,依赖扫描自动修复目前处于公开测试阶段,它借助 AI 修复导致构建失败的更改,持续…

2026/7/24 5:13:42 阅读更多 →

日新闻

用Highcharts 创建可拖拽三维散点立方体3D图表

用Highcharts 创建可拖拽三维散点立方体3D图表

该案例基于Highcharts scatter3d 三维散点图实现空间立方体散点可视化,核心特色:三维 X/Y/Z 三轴空间,所有散点分布在 0~10 立方体空间内;散点使用径向渐变实现立体 3D 圆球质感;支持鼠标 / 触屏拖拽画布,…

2026/7/24 0:00:29 阅读更多 →
AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口 AppCertDlls 位于 HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值,并显示每条值的来源、名称、类型和可安全显示的数…

2026/7/24 0:00:29 阅读更多 →
我的编程之路:第一篇博客

我的编程之路:第一篇博客

大家好,我是一名编程初学者,同时这也是我编程学习之路上的第一篇博客。在这里,我想要向大家介绍我的一些想法和规划。a.自我介绍我是一个刚刚接触编程的新手,目前在学习c语言,我对编程世界充满了强烈的好奇。当然&…

2026/7/24 0:00:29 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/24 3:59:20 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/24 1:23:39 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻