本文目录导读:

我来为你提供一个完整的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,也可以使用内存版本实现,只需调整配置即可。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播