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

}

两者的使用场景

相关推荐
Sam_Deep_Thinking1 小时前
一次线程池线上故障复盘:四层防线如何避免数据丢失
java·面试·程序员
paopaokaka_luck1 小时前
基于Springboot3+Vue3的宠物殡葬管理系统(webSocket实时通讯、接入腾讯地图、支付宝沙盒支付、Echarts图形化分析)
java·开发语言·网络·后端
卷无止境1 小时前
Python 中基于 Qt 的 GUI 库授权方式全解析
后端·python
程序员爱钓鱼1 小时前
Rust 注释与文档注释详解:从代码说明到 cargo doc
后端·rust
前鼻音太阳熊2 小时前
【MES系统】MES为什么需要SSE?从设备实时监控谈Spring Boot流式推送设计
java·spring boot·后端
Lhappy嘻嘻2 小时前
网络初识|基础入门:局域网广域网、IP 端口、TCP/IP 五层模型与封装解封装全过程
java·开发语言·网络·笔记·网络协议·tcp/ip
卷无止境2 小时前
Guardrails.ai:为大语言模型加一道"安全阀"
后端·python
Java面试题总结2 小时前
区间树:反向映射的前置数据结构
java·前端·数据结构
尚早立志2 小时前
六)Spring Boot 源码研读之prepareContext 准备上下文
java·spring boot·后端·spring