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);

}

两者的使用场景

相关推荐
n8n3 小时前
多 Agent 协作实战:Supervisor 模式构建专家团队,突破单一 Agent 能力边界
后端
BingoGo3 小时前
PHP Trait 为何可能成为语言的关键特性
后端·php
Java_2017_csdn3 小时前
StringUtils.hasText() 和 StringUtils.isNotBlank() 方法对比
java
Joy T3 小时前
Spring AI 2.0 进阶入门:RAG、Structured Output 与 Agent 信息闭环
java·人工智能·spring·rag·springai·agent入门
名字还没想好☜3 小时前
Python 的 __call__ 实战:让实例像函数一样被调用,做带状态计数器、缓存器与可配置策略
开发语言·后端·python·缓存·编程语言
xiaoqiMikko3 小时前
jackson-databind 又出 4 条,Dependabot 一条都不报:2.21.5 还差 3 条
java·安全
杨利杰YJlio3 小时前
ITSK PE 26U5 测试版解读:组件完善、VMD 修复与服务器支持边界
前端·javascript·后端
PragmaticWorks3 小时前
充血模型该充到什么地步?
后端·领域驱动设计
無限進步D3 小时前
VSCode 简介+安装+JavaWeb前端常用插件+简单配置
java·开发语言·css·vscode·html·css3·插件
xiaoqiMikko3 小时前
Central 上的 Spring 5.3.41 不是官方发的:逐字节比对,官方修两条它少一条
java·安全