RequestBodyAdviceAdapter类
RequestBodyAdviceAdapter是 Spring MVC 框架提供的一个抽象类,它实现了RequestBodyAdvice接口,并为该接口的所有方法提供了默认的空实现。
它的主要作用是作为一个便捷的基类,让你能够拦截并自定义处理带有@RequestBody或HttpEntity参数的请求。通过继承它,你只需重写自己关心的方法,而无需实现接口中所有的方法。
核心代码
/**
* 重复提交拦截器 如果涉及前后端加解密的话 也可以通过继承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);
}
两者的使用场景
