本文目录导读:

我来为你提供一个完整的Spring Boot防重复提交的解决方案。
项目结构
src/main/java/com/example/demo/
├── annotation/
│ └── NoRepeatSubmit.java
├── aspect/
│ └── NoRepeatSubmitAspect.java
├── controller/
│ └── TestController.java
├── dto/
│ └── Result.java
└── DemoApplication.java
依赖配置
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>repeat-submit</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>repeat-submit</name>
<description>防重复提交示例</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 8080
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
# 自定义配置
repeat-submit:
# 是否开启防重复提交
enabled: true
# 重复提交检测时间间隔(秒)
interval: 10
# 使用哪种存储方式:redis / memory
storage: redis
核心代码实现
1 自定义注解
package com.example.demo.annotation;
import java.lang.annotation.*;
/**
* 防重复提交注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmit {
/**
* 重复提交时间间隔(秒)
* 默认10秒
*/
int interval() default 10;
/**
* 提示消息
*/
String message() default "请勿重复提交,请稍后再试";
/**
* 是否只对登录用户生效
* 如果为true,则会结合用户ID生成唯一key
*/
boolean onlyUser() default false;
}
2 统一返回结果
package com.example.demo.dto;
import lombok.Data;
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
return result;
}
}
3 防重复提交切面实现
package com.example.demo.aspect;
import com.alibaba.fastjson.JSON;
import com.example.demo.annotation.NoRepeatSubmit;
import com.example.demo.dto.Result;
import com.example.demo.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
@Slf4j
@Aspect
@Component
public class NoRepeatSubmitAspect {
@Autowired
private RedisUtil redisUtil;
/**
* 环绕通知,拦截带有@NoRepeatSubmit注解的方法
*/
@Around("@annotation(com.example.demo.annotation.NoRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取HttpServletRequest
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
HttpServletRequest request = attributes.getRequest();
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
NoRepeatSubmit noRepeatSubmit = method.getAnnotation(NoRepeatSubmit.class);
// 生成请求唯一标识
String redisKey = generateRedisKey(request, joinPoint, noRepeatSubmit);
// 生成唯一token
String token = generateToken(request);
// 检查是否重复提交
if (!redisUtil.trySet(redisKey, token, noRepeatSubmit.interval())) {
log.warn("检测到重复提交,key: {}", redisKey);
return Result.error(500, noRepeatSubmit.message());
}
try {
// 执行目标方法
Object result = joinPoint.proceed();
// 获取请求中的token,用于后续校验
setRequestToken(request, token);
return result;
} catch (Exception e) {
log.error("请求处理异常", e);
throw e;
} finally {
// 请求完成后删除key(可选)
// 如果需要立即删除key,可以取消注释下面的代码
// redisUtil.delete(redisKey);
}
}
/**
* 生成Redis Key
*/
private String generateRedisKey(HttpServletRequest request,
ProceedingJoinPoint joinPoint,
NoRepeatSubmit noRepeatSubmit) {
// 基础信息
String uri = request.getRequestURI();
String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." +
joinPoint.getSignature().getName();
// 用户ID(可选)
String userId = "";
if (noRepeatSubmit.onlyUser()) {
userId = getUserIdFromRequest(request);
}
// 请求参数
Object[] args = joinPoint.getArgs();
String params = args.length > 0 ? JSON.toJSONString(args) : "";
// 生成签名
String content = uri + methodName + userId + params;
String hashCode = String.valueOf(content.hashCode());
// 组合Key
return "repeat_submit:" + hashCode;
}
/**
* 从请求中获取用户ID
*/
private String getUserIdFromRequest(HttpServletRequest request) {
// 从header或session中获取用户信息
String userId = request.getHeader("userId");
if (userId == null || userId.isEmpty()) {
// 从session获取
Object user = request.getSession().getAttribute("userId");
if (user != null) {
userId = user.toString();
}
}
return userId == null ? "" : userId;
}
/**
* 生成请求唯一token
*/
private String generateToken(HttpServletRequest request) {
String token = (String) request.getAttribute("repeat_token");
if (token == null) {
token = UUID.randomUUID().toString().replace("-", "");
request.setAttribute("repeat_token", token);
}
return token;
}
/**
* 设置请求token到响应header
*/
private void setRequestToken(HttpServletRequest request, String token) {
request.setAttribute("repeat_token", token);
}
}
4 Redis工具类
package com.example.demo.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 尝试设置key-value,如果key已存在则返回false
* 使用SETNX命令实现原子性操作
*/
public boolean trySet(String key, String value, long timeout) {
Boolean result = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 设置key-value
*/
public void set(String key, String value, long timeout) {
stringRedisTemplate.opsForValue()
.set(key, value, timeout, TimeUnit.SECONDS);
}
/**
* 获取value
*/
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 删除key
*/
public boolean delete(String key) {
Boolean result = stringRedisTemplate.delete(key);
return Boolean.TRUE.equals(result);
}
/**
* 判断key是否存在
*/
public boolean hasKey(String key) {
Boolean result = stringRedisTemplate.hasKey(key);
return Boolean.TRUE.equals(result);
}
}
5 内存版存储(可选,不依赖Redis时使用)
package com.example.demo.util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 内存版防重复提交存储
* 适用于单机部署场景
*/
@Component
public class MemoryCacheUtil {
@Value("${repeat-submit.interval:10}")
private long defaultExpireTime;
private final Map<String, CacheItem> cache = new ConcurrentHashMap<>();
/**
* 尝试设置key,如果key已存在则返回false
*/
public boolean trySet(String key, String value, long expireTime) {
long now = System.currentTimeMillis();
CacheItem item = new CacheItem(value, now + expireTime * 1000);
synchronized (this) {
// 先清理过期数据
cleanExpired();
CacheItem existing = cache.get(key);
if (existing == null || existing.isExpired()) {
cache.put(key, item);
return true;
}
return false;
}
}
/**
* 清理过期数据
*/
private void cleanExpired() {
long now = System.currentTimeMillis();
cache.entrySet().removeIf(entry -> entry.getValue().isExpired());
}
/**
* 缓存条目
*/
private static class CacheItem {
private final String value;
private final long expireTime;
public CacheItem(String value, long expireTime) {
this.value = value;
this.expireTime = expireTime;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
}
6 测试Controller
package com.example.demo.controller;
import com.example.demo.annotation.NoRepeatSubmit;
import com.example.demo.dto.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@RestController
@RequestMapping("/api")
public class TestController {
/**
* 测试防重复提交
*/
@PostMapping("/submit")
@NoRepeatSubmit(interval = 10, message = "提交太频繁,请10秒后再试")
public Result<Map<String, Object>> submit(@RequestBody Map<String, Object> params) {
log.info("处理请求:{}", params);
// 模拟业务处理
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Map<String, Object> result = new HashMap<>();
result.put("code", "200");
result.put("message", "请求成功");
result.put("data", params);
return Result.success(result);
}
/**
* 测试登录用户防重复提交
*/
@PostMapping("/user/submit")
@NoRepeatSubmit(onlyUser = true, interval = 30, message = "操作太频繁,请30秒后再试")
public Result<Map<String, Object>> userSubmit(@RequestBody Map<String, Object> params) {
log.info("处理用户请求:{}", params);
Map<String, Object> result = new HashMap<>();
result.put("code", "200");
result.put("message", "用户请求成功");
result.put("data", params);
return Result.success(result);
}
/**
* 测试获取请求token
*/
@GetMapping("/token")
public Result<String> getToken() {
// 简单演示,实际项目中token应该从请求中获取
return Result.success("token_" + System.currentTimeMillis());
}
}
7 启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
测试方式
1 使用Postman测试
-
正常请求
- POST
http://localhost:8080/api/submit - Body:
{"name":"test", "value":"123"} - 期望返回:
{"code":200,"message":"success","data":{...}}
- POST
-
重复请求
- 短时间内再次发送相同的POST请求
- 期望返回:
{"code":500,"message":"提交太频繁,请10秒后再试"}
2 使用curl测试
# 第一次请求 - 成功
curl -X POST http://localhost:8080/api/submit \
-H "Content-Type: application/json" \
-d '{"name":"test","value":"123"}'
# 第二次请求 - 触发防重复
curl -X POST http://localhost:8080/api/submit \
-H "Content-Type: application/json" \
-d '{"name":"test","value":"123"}'
优化建议
1 增加全局异常处理
package com.example.demo.config;
import com.example.demo.dto.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) {
return Result.error(500, "系统错误:" + e.getMessage());
}
}
2 使用分布式锁增强
@Aspect
@Component
public class DistributedLockAspect {
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {
// 获取锁
String lockKey = generateLockKey(joinPoint);
boolean locked = tryLock(lockKey);
if (!locked) {
throw new RuntimeException("系统繁忙,请稍后再试");
}
try {
return joinPoint.proceed();
} finally {
// 释放锁
unlock(lockKey);
}
}
}
这样实现的防重复提交功能具有以下特点:
- 基于Redis实现,支持分布式环境
- 使用AOP切面,非侵入式设计
- 支持自定义配置,灵活可扩展
- 自动生成唯一Key,基于请求参数
- 支持用户隔离,可针对不同用户
如果你的应用不需要Redis,也可以使用内存版本实现,只需调整配置即可。