在SpringBoot使用AOP防止接口重复提交

前言

防止接口重复提交有跟多种方法,可以在前端做处理。同样在后端也能处理,而且后端的处理也有很多中方法。最先能想到的就是加锁,也可以直接在该接口的实现过程中进行处理(可以参考防止数据重复提交的6种方法(超简单)!),本文主要介绍另一种借助AOP实现的方法。

AOP

关于AOP就不做过多赘述,可以参考我的另一篇文章Spring框架(下半部分 -AOP)。主要是借助它能增强方法的功能,对接口做以下处理,这个方法跟直接在接口种处理相似,话不多说,我们直接开始吧。

自定义注解

我们要灵活的使用AOP,注解是必不可少的,能帮我们更加便捷灵活的处理。我们先创建一个Submit注解,有该注解的接口就是我们要使用AOP处理的接口。

package com.blog.annotation;


import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME) // 注解的存活时间
@Target(ElementType.METHOD) // 作用在方法上
public @interface Submit {
    /**
     * 提交的间隔时间
     * 默认是10s
     * @return
     */
    long expire() default 10000;
}

AOP的实现

其实使用AOP都有一个很创建的模板,我先贴出来,然后解释。

@Aspect
@Component
@Slf4j
public class SubmitAspect {

    @Pointcut("@annotation(com.blog.annotation.Submit)")
    public void pt() {
    
    }
    
    @Around("pt()")
    public Object around(ProceedingJoinPoint point) {
    
    }
}

@Pointcut("@annotation(com.blog.annotation.Submit)")就是切入点表达式,它的参数就是指定我们要处理,@Around("pt()")表明我们使用环绕通知来处理。具体的在我刚刚提到的另一篇博客中,感兴趣的可以仔细的了解一下。

接下来我们就要考虑该如何实现,防止接口重复提交就是说如果该接口提交过了,再来一次提交我们就不让他去执行,直接返回。现在就有一个问题了,我们该如何知道这个接口提交没提交过?我们是不是可以把提交过的接口保存下来,如果来了一个提交我们就去查找,如果找到了我们就不如他提交。

if (接口 not in 接口集合) {
	return "请勿重复提交";
}
// 说明接口没有提交,我们就执行该接口的方法
// 最重要的一点是把该接口存储到接口集合中
...执行提交操作...
接口集合.insert(接口)

所以我们就需要考虑使用哪些集合?这个接口该怎么存储?怎么执行原方法的操作?什么时候用户还能再次提交代码?等等,这些都是我们要考虑的问题。

关于集合的使用,我们首先能想到的是list、set、map等等,但是考虑到并发安全,我们应该使用线程安全的集合例如ConcurrentHashMap、CopyOnWriteArrayList等等。我们还要解决什么时候用户还能再次提交代码,我们可以设置一个实现,所以更加推荐ConcurrentHashMap,其key值就是我们为每一个接口构建的key(使用类名+方法名),value就是我们设置的时间。

想到这还有一个问题,我们为每一个接口构建key,如果有多个用户那么他们的key就是一样的,可事实上每个用户的同一接口的key一定是不能一样的,否则他提交了我提交不了,这凭什么?所以我们再构建每一个接口的key时加上当前用户的唯一标识,使用该用户的id就行。

那么又该如何获取到当前用户的id呢? 在这里我们ThreadLocal就可以,ThreadLocal也是很重要的,如果不是很了解,建议花点时间去认识它。在这里我们只需要知道,他是独立于线程之外的,每一个线程又一个独自的ThreadLocal ,也就是说,我们把每一个用户都存储在ThreadLocal 中。要的时候直接get就行。

到这里其实核心的问题都已经解决了,剩下的就是一些细节问题,在自己写的时候就能注意到。这里给出我的实现。我使用的redis实现,因为它设置过期时间会自动清除,不需要我们手动去清除,再加上redis是天生支持高并发。
SUBMIT_KEY_PREFIX和NOT_SUBMIT_REPEATEDLY都是一个常量而已,不用过多注意。

package com.blog.aspect;

import com.alibaba.fastjson.JSON;
import com.blog.annotation.Submit;
import com.blog.utils.JWTUtils;
import com.blog.utils.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;

import static com.blog.domain.vo.ErrorCode.NOT_SUBMIT_REPEATEDLY;
import static com.blog.utils.ConstantValue.SUBMIT_KEY_PREFIX;

@Aspect
@Component
@Slf4j
public class SubmitAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Pointcut("@annotation(com.blog.annotation.Submit)")
    public void pt() {
    }


    @Around("pt()")
    public Object around(ProceedingJoinPoint point) {
        try {
//            User user = UserThreadLocal.get();
//            String UserId = user.getId();
            // 假设这里是从ThreadLocal获取到的用户id。
            String UserId = "123456";
            Signature signature = point.getSignature();
            // 获取当前类名
            String className = point.getTarget().getClass().getSimpleName();
            // 获取当前方法名
            String methodName = signature.getName();
            // 拿到该方法
            Method method = ((MethodSignature) signature).getMethod();
            // 获取Submit注解
            Submit annotation = method.getAnnotation(Submit.class);
            // 获取过期时间
            long expire = annotation.expire();

            // 设置key值,每个用户对与每一个接口的key都是一样的
            String key = SUBMIT_KEY_PREFIX + DigestUtils.md5Hex(UserId) + "::" + className + "::" + methodName;

            // 首先查看是否已经提交过
            String value = redisTemplate.opsForValue().get(key);
            if (StringUtils.isNoneEmpty(value)) {
                return Result.error(NOT_SUBMIT_REPEATEDLY.getCode(), NOT_SUBMIT_REPEATEDLY.getMsg());
            }

            // 没有提交过就执行原方法
            Object proceed = point.proceed();
            redisTemplate.opsForValue().set(key, JSON.toJSONString(proceed), Duration.ofMillis(expire));
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.error(-999, "系统异常");
    }
}

测试

接下来我们使用ApiPost进行测试,由于我们给定了id,所以我们只能测试单用户的,如果想测试多用户的,可以在请求路径中加上一个id,来模拟多用户。

间隔0ms,调用5次,只有一次成功,失败的几次,这里就不截图了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e2fHCLrs-1720583474961)(https://i-blog.csdnimg.cn/direct/e0b09889def54e4494172c9edc1571e1.png)]

间隔11000ms,调用2次,每次都成功,这是因为我们的冷静窗口是10000ms。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zG3MVA4q-1720583474962)(https://i-blog.csdnimg.cn/direct/a0153a775a524fd0821df825fa3154ff.png)]

相关推荐

  1. SpringBoot使用AOP防止接口重复提交

    2024-07-11 20:50:06       23 阅读
  2. SpringBoot防止重复提交 AOP+自定义注解+redis

    2024-07-11 20:50:06       23 阅读
  3. SpringBoot表单防止重复提交

    2024-07-11 20:50:06       36 阅读
  4. springboot防止表单重复提交

    2024-07-11 20:50:06       27 阅读
  5. 通过Redis实现防止接口重复提交功能

    2024-07-11 20:50:06       28 阅读
  6. 模拟防止重复提交

    2024-07-11 20:50:06       21 阅读
  7. 002 springboot redis 防止表单重复提交

    2024-07-11 20:50:06       25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 20:50:06       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 20:50:06       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 20:50:06       58 阅读
  4. Python语言-面向对象

    2024-07-11 20:50:06       69 阅读

热门阅读

  1. Spring AOP的几种实现方式

    2024-07-11 20:50:06       19 阅读
  2. pytorch 模型保存到本地之后,如何继续训练

    2024-07-11 20:50:06       23 阅读
  3. 【Spring】springSecurity使用

    2024-07-11 20:50:06       17 阅读
  4. 力扣682.棒球比赛

    2024-07-11 20:50:06       18 阅读
  5. STM32学习历程(day4)

    2024-07-11 20:50:06       21 阅读
  6. C# 装饰器模式(Decorator Pattern)

    2024-07-11 20:50:06       21 阅读
  7. 代码随想录-DAY⑦-字符串——leetcode 344 | 541 | 151

    2024-07-11 20:50:06       21 阅读
  8. FastAPI+SQLAlchemy数据库连接

    2024-07-11 20:50:06       19 阅读
  9. 关于vue监听数组

    2024-07-11 20:50:06       18 阅读
  10. SQL 自定义函数

    2024-07-11 20:50:06       22 阅读
  11. linux内核访问读写用户层文件方法

    2024-07-11 20:50:06       21 阅读
  12. RK3568平台开发系列讲解(网络篇)netfilter框架

    2024-07-11 20:50:06       19 阅读
  13. Netty服务端接收TCP链接数据

    2024-07-11 20:50:06       16 阅读