欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Boot使用AOP防止重复提交的方法示例

程序员文章站 2023-11-12 20:43:04
在传统的web项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保...

在传统的web项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保证提交请求的唯一性。

上述的思路其实没有问题的,但是需要前后端都稍加改动,如果在业务开发完在加这个的话,改动量未免有些大了,本节的实现方案无需前端配合,纯后端处理。

思路

  1. 自定义注解 @norepeatsubmit 标记所有controller中的提交请求
  2. 通过aop 对所有标记了 @norepeatsubmit 的方法拦截
  3. 在业务方法执行前,获取当前用户的 token(或者jsessionid)+ 当前请求地址,作为一个唯一 key,去获取 redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)
  4. 业务方法执行后,释放锁

关于redis 分布式锁

不了解的同学戳这里 ==> redis分布式锁的正确实现方式

使用redis 是为了在负载均衡部署,如果是单机的部署的项目可以使用一个线程安全的本地cache 替代 redis

code

这里只贴出 aop 类和测试类,完整代码见 ==> gitee

@aspect
@component
public class repeatsubmitaspect {

  private static final logger logger = loggerfactory.getlogger(repeatsubmitaspect.class);

  @autowired
  private redislock redislock;

  @pointcut("@annotation(com.gitee.taven.aop.norepeatsubmit)")
  public void pointcut() {}

  @around("pointcut()")
  public object before(proceedingjoinpoint pjp) {
    try {
      httpservletrequest request = requestutils.getrequest();
      assert.notnull(request, "request can not null");

      // 此处可以用token或者jsessionid
      string token = request.getheader("authorization");
      string path = request.getservletpath();
      string key = getkey(token, path);
      string clientid = getclientid();

      boolean issuccess = redislock.trylock(key, clientid, 10);
      logger.info("trylock key = [{}], clientid = [{}]", key, clientid);

      if (issuccess) {
        logger.info("trylock success, key = [{}], clientid = [{}]", key, clientid);
        // 获取锁成功, 执行进程
        object result = pjp.proceed();
        // 解锁
        redislock.releaselock(key, clientid);
        logger.info("releaselock success, key = [{}], clientid = [{}]", key, clientid);
        return result;

      } else {
        // 获取锁失败,认为是重复提交的请求
        logger.info("trylock fail, key = [{}]", key);
        return new apiresult(200, "重复请求,请稍后再试", null);
      }

    } catch (throwable throwable) {
      throwable.printstacktrace();
    }

    return new apiresult(500, "系统异常", null);
  }

  private string getkey(string token, string path) {
    return token + path;
  }

  private string getclientid() {
    return uuid.randomuuid().tostring();
  }

}

多线程测试

测试代码如下,模拟十个请求并发同时提交

@component
public class runtest implements applicationrunner {

  private static final logger logger = loggerfactory.getlogger(runtest.class);

  @autowired
  private resttemplate resttemplate;

  @override
  public void run(applicationarguments args) throws exception {
    system.out.println("执行多线程测试");
    string url="http://localhost:8000/submit";
    countdownlatch countdownlatch = new countdownlatch(1);
    executorservice executorservice = executors.newfixedthreadpool(10);

    for(int i=0; i<10; i++){
      string userid = "userid" + i;
      httpentity request = buildrequest(userid);
      executorservice.submit(() -> {
        try {
          countdownlatch.await();
          system.out.println("thread:"+thread.currentthread().getname()+", time:"+system.currenttimemillis());
          responseentity<string> response = resttemplate.postforentity(url, request, string.class);
          system.out.println("thread:"+thread.currentthread().getname() + "," + response.getbody());

        } catch (interruptedexception e) {
          e.printstacktrace();
        }
      });
    }

    countdownlatch.countdown();
  }

  private httpentity buildrequest(string userid) {
    httpheaders headers = new httpheaders();
    headers.setcontenttype(mediatype.application_json);
    headers.set("authorization", "yourtoken");
    map<string, object> body = new hashmap<>();
    body.put("userid", userid);
    return new httpentity<>(body, headers);
  }

}

成功防止重复提交,控制台日志如下,可以看到十个线程的启动时间几乎同时发起,只有一个请求提交成功了

Spring Boot使用AOP防止重复提交的方法示例

本节demo

戳这里 ==> gitee

build项目之后,启动本地redis,运行项目自动执行测试方法

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。