SpringBoot后端限制重复提交

RequestBodyAdviceAdapter类

RequestBodyAdviceAdapter 是 Spring MVC 框架提供的一个抽象类,它实现了 RequestBodyAdvice 接口,并为该接口的所有方法提供了默认的空实现。
它的主要作用是作为一个便捷的基类,让你能够拦截并自定义处理带有 @RequestBodyHttpEntity 参数的请求。通过继承它,你只需重写自己关心的方法,而无需实现接口中所有的方法。

核心代码

复制代码
/**
 * 重复提交拦截器 如果涉及前后端加解密的话  也可以通过继承RequestBodyAdvice来实现
 *
 * @author valarchie
 */
@ControllerAdvice(basePackages = "com.wudi")
@Slf4j
@RequiredArgsConstructor
public class UnrepeatableInterceptor extends RequestBodyAdviceAdapter {

    private final RedisUtil redisUtil;

// 判断是否需要拦截:只有方法上标了@Unrepeatable注解的才处理
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Unrepeatable.class);
    }

    /**
     * @param body 仅获取有RequestBody注解的参数
     */
    @NotNull
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅获取有RequestBody注解的参数
       // 1️⃣ 把当前请求参数转成JSON字符串(相当于"拍照留证")
        String currentRequest = JSONUtil.toJsonStr(body);

        Unrepeatable resubmitAnno = parameter.getMethodAnnotation(Unrepeatable.class);
        if (resubmitAnno != null) {
 // 2️⃣ 获取注解配置,生成Redis键名
            String redisKey = resubmitAnno.checkType().generateResubmitRedisKey(parameter.getMethod());

            log.info("请求重复提交拦截,当前key:{}, 当前参数:{}", redisKey, currentRequest);
// 3️⃣ 从Redis取出上一次的请求记录
            String preRequest = redisUtil.getCacheObject(redisKey);
            if (preRequest != null) {
                boolean isSameRequest = Objects.equals(currentRequest, preRequest);
 // 4️⃣ 对比两次请求是否完全一样
                if (isSameRequest) {
 // 重复了!拒绝处理
                    throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);
                }
            }
// 5️⃣ 不重复,把这次请求存到Redis,设置过期时间
            redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);
        }

        return body;
    }

}

Redis 中的set和setnx

Redis 的 SET

无论 key 是否存在,都会写入/覆盖

这里的逻辑是:先 GET 查一下有没有旧值 → 比较内容 → 再 SET 写入新值

这是一个 "读-比较-写" 三步操作,不是原子的

Redis的 SET if Not eXists

只有 key 不存在时才写入,已存在则写入失败

是原子操作,一步完成"检查+写入"

用setNull改造后的时序图

SetNx常见的坑以及注意的点

// 原子操作:尝试写入,成功说明是首次请求,失败说明key已存在

Boolean isFirst = redisUtil.setIfAbsent(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);

if (Boolean.FALSE.equals(isFirst)) {

// key已存在,再比较内容确认是否真的重复

String preRequest = redisUtil.getCacheObject(redisKey);

if (Objects.equals(currentRequest, preRequest)) {

throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);

}

// 内容不同则更新为最新请求

redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);

}

两者的使用场景

相关推荐
AskHarries14 小时前
OpenClaw 如何查看历史对话和响应时间?
后端
n8n14 小时前
Spring Boot 3.2+ 新特性、Spring AI 核心架构与 API Key 安全管理技术文档
后端
古法安卓14 小时前
Android-epoll原理详解
android·java·android studio
fireworks9914 小时前
接口鉴权(401与403)
java·后端
wear工程师14 小时前
线程池里的异常去哪了?execute 和 submit 不是一回事
java
n8n14 小时前
Java 21 虚拟线程(Virtual Threads)
后端
孔明click3314 小时前
别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权
java·sa-token·springboot·权限·权限认证
吴声子夜歌14 小时前
Java面试——设计模式(一)
java·设计模式·面试
Csvn14 小时前
🐍 Day 5: Python 函数详解 — 参数、作用域与一等公民
人工智能·后端
半亩码田14 小时前
C#转Python第3.6篇:Python 的 @property 比 C# 的 get/set 更灵活
java·python·c#