微服务中使用AOP实现防止接口二次提交

本文介绍了一种防止接口二次提交的有效方法,利用AOP和Redis实现。通过在新增、修改接口上使用自定义注解,结合Redis缓存判断请求是否重复,有效避免了同一操作的重复执行。

无论在微服务还是单体程序中,防止接口二次提交都是必须要解决方法,现在已经有成熟的解决方案,比如采用点击一次后让按钮置灰,等请求结束后再可以点击。当然后端也要解决这个问题。

采用AOP的方式防止接口二次提交
  • 思路
    • 以唯一标识为key,任意值为value,存入redis(本地缓存也可以),并设置一个合理的过期时间。
    • 将注解用在新增、修改等接口上。
    • 每次调用时根据key判断,缓存是否存在,存在则抛出异常或提示,不存在则执行业务逻辑。
  • 定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmitCheck {
    int keepSeconds() default 5;
}
  • 实现这个注解
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Resource
    private RedisService redisService;
 
    @Pointcut("@annotation(com.xxx.xxx.annotation.RepeatSubmitCheck)")
    public void requestPointcut() {
    }
 
    @Around("requestPointcut()")
    public void aroundCheck(JoinPoint joinPoint, RepeatSubmitCheck repeatSubmitCheck) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String paramsJsonStr = JSON.toJSONString(args);
 
        String srcStr = className + "_" + methodName + "_" + paramsJsonStr;
        String signStr = Md5Utils.md5(srcStr);
        log.info("防止重复提交的请求 Redis Key:" + signStr);
 
        //判断缓存是否存在不存在则添加缓存
        if(!redisService.exists(signStr)){
            redisService.setex(signStr, repeatSubmitCheck.keepSeconds(), "1");
        } else {//重复请求
            log.info("重复提交的请求数据:" + srcStr);
            throw new RuntimeException("重复请求");
        }
    }
}
  • RedisService部分代码

@AllArgsConstructor
public class RedisService {

	private RedisTemplate<String, Object> redisTemplate;

	public boolean exists(String key) {
        return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            return connection.exists(key.getBytes());
        });
    }
    
	public boolean setex(final String key, long expire, final String value) {
        return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
            return connection.setEx(serializer.serialize(key), expire, serializer.serialize(value));
        });
    }
}
  • 使用方法
@PostMapping("/repeatSubmit")
@RepeatSubmitCheck
public String testRepeatSubmit() {
    return "test RepeatSubmitCheck";
}
### 防止Java接口重复调用的最佳实践 在Java开发中,防止接口被重复调用是一项重要任务,尤其是在涉及金融交易、订单管理等场景时。以下是几种常见的解决方案: #### 方法一:基于数据库唯一约束 可以通过设置数据库字段的唯一性来防止重复请求。例如,在处理订单支付时,可以将订单号作为唯一的标识符存储到数据库中。如果检测到相同的订单号已经存在,则拒绝再执行该操作。 ```sql ALTER TABLE orders ADD CONSTRAINT unique_order_number UNIQUE (order_number); ``` 当接收到一个新的请求时,先尝试插入记录;如果违反了唯一键约束,则捕获异常并返回错误提示[^4]。 #### 方法二:利用分布式锁机制 使用Redis或其他支持分布式的缓存工具实现加锁功能是一种高效的方式。具体来说,每调用API之前都会生成一个随机令牌(Token),并将此Token保存至Redis中设定过期时间。后续任何带有同样Token的新请求都将被拦截掉直到原操作完成为止。 ```java String lockKey = "lock:" + userId; Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", Duration.ofSeconds(30)); if (!isLocked) { throw new RuntimeException("Duplicate request detected."); } try { // 执行业务逻辑... } finally { redisTemplate.delete(lockKey); // 解除锁定 } ``` 这种方法不仅能够有效避免并发环境下的竞争条件问题,而且还能很好地适应微服务架构下跨节点间的协调需求[^5]。 #### 方法三:引入中间件层过滤器 对于Spring Boot应用程序而言,可以在全局范围内配置HttpServletResponseWrapper类实例化对象包裹原始response流,并覆盖其getOutputStream()和getWriter()两个核心方法。如此一来便能够在实际响应数据写出前增加额外校验步骤——比如检查是否存在未结束的任务队列项等等。 另外一种更简便的做法就是借助于Spring AOP切面编程技术配合自定义注解共同作用达成目的。如下所示即为一段典型代码片段演示如何通过声明式事务管理和环绕通知组合起来达到预期效果: ```java @Aspect @Component public class DuplicateSubmissionInterceptor { @Around("@annotation(preventResubmission)") public Object preventDuplicateSubmissions(ProceedingJoinPoint joinPoint, PreventResubmission preventResubmission) throws Throwable { String key = generateUniqueKey(joinPoint); if (cacheService.exists(key)) { log.warn("Detected duplicate submission attempt for {}", key); return ResponseEntity.status(HttpStatus.CONFLICT).body("Request already processed"); } try { cacheService.setExpiry(key,preventResubmission.expiryTime(), TimeUnit.SECONDS); return joinPoint.proceed(); } catch (Exception e){ throw e; }finally{ cacheService.remove(key); } } private String generateUniqueKey(ProceedingJoinPoint joinPoint){ StringBuilder sb=new StringBuilder(); MethodSignature signature=(MethodSignature)joinPoint.getSignature(); sb.append(signature.getName()).append(Arrays.toString(joinPoint.getArgs())); return DigestUtils.md5DigestAsHex(sb.toString().getBytes()); } } // 自定义注解用于标记需要防护的方法 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface PreventResubmission { int expiryTime() default 60 ; } ``` 上述例子展示了如何结合AOP与缓存服务阻止不必要的二次提交行为发生的同时还允许开发者灵活指定失效周期长短以便满足不同应用场景的要求[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值