Springboot2本地锁实践

  在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用户可能会以为是自己没有提交表单,就会再点击提交按钮重复提交表单,我们在开发中必须防止表单重复提交….

下面我们利用自定义注解Spring AopGuava Cache 实现表单防重复提交

一、导入依赖

 创建springboot项目,在pom.xml文件中加入以下内容

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>21.0</version>
    </dependency>
</dependencies>

 二、Lock注解

创建一个 LocalLock 注解,就一个 key 可以了

 1 package com.carry.annotation;
 2 
 3 import java.lang.annotation.Documented;
 4 import java.lang.annotation.ElementType;
 5 import java.lang.annotation.Inherited;
 6 import java.lang.annotation.Retention;
 7 import java.lang.annotation.RetentionPolicy;
 8 import java.lang.annotation.Target;
 9 
10 /**
11  * 锁的注解
12  *
13  */
14 @Target(ElementType.METHOD)
15 @Retention(RetentionPolicy.RUNTIME)
16 @Documented
17 @Inherited
18 public @interface LocalLock {
19 
20     String key() default "";
21 }

 三、Lock拦截器(AOP)

首先通过 CacheBuilder.newBuilder() 构建出缓存对象,设置好过期时间;其目的就是为了防止因程序崩溃锁得不到释放,然后在具体的 interceptor() 方法上采用的是 Around(环绕增强) ,所有带 LocalLock 注解的都将被切面处理

具体代码

 1 package com.carry.interceptor;
 2 
 3 import java.lang.reflect.Method;
 4 import java.util.concurrent.TimeUnit;
 5 import org.aspectj.lang.ProceedingJoinPoint;
 6 import org.aspectj.lang.annotation.Around;
 7 import org.aspectj.lang.annotation.Aspect;
 8 import org.aspectj.lang.reflect.MethodSignature;
 9 import org.springframework.context.annotation.Configuration;
10 import org.springframework.util.StringUtils;
11 import com.carry.annotation.LocalLock;
12 import com.google.common.cache.Cache;
13 import com.google.common.cache.CacheBuilder;
14 
15 @Aspect
16 @Configuration
17 public class LockMethodInterceptor {
18 
19     private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
20             // 最大缓存 100 个
21             .maximumSize(100)
22             // 设置写缓存后 5 秒钟过期
23             .expireAfterWrite(5, TimeUnit.SECONDS).build();
24 
25     @Around("execution(public * *(..)) && @annotation(com.carry.annotation.LocalLock)")
26     public Object interceptor(ProceedingJoinPoint pjp) {
27         MethodSignature signature = (MethodSignature) pjp.getSignature();
28         Method method = signature.getMethod();
29         LocalLock localLock = method.getAnnotation(LocalLock.class);
30         String key = getKey(localLock.key(), pjp.getArgs());
31         if (!StringUtils.isEmpty(key)) {
32             if (CACHES.getIfPresent(key) != null) {
33                 throw new RuntimeException("请勿重复请求");
34             }
35             // 如果是第一次请求,就将 key 当前对象压入缓存中
36             CACHES.put(key, key);
37         }
38         try {
39             return pjp.proceed();
40         } catch (Throwable throwable) {
41             throw new RuntimeException("服务器异常");
42         } finally {
43             // TODO
44         }
45     }
46 
47     /**
48      * key 的生成策略,如果想灵活可以写成接口与实现类的方式
49      *
50      * @param keyExpress
51      *            表达式
52      * @param args
53      *            参数
54      * @return 生成的key
55      */
56     private String getKey(String keyExpress, Object[] args) {
57         for (int i = 0; i < args.length; i++) {
58             keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
59         }
60         return keyExpress;
61     }
62 }

四、控制层

在接口方法上添加 @LocalLock(key = "book:arg[0]");意味着会将 arg[0] 替换成第一个参数的值,生成后的新 key 将被缓存起来

具体代码

 1 package com.carry.controller;
 2 
 3 import org.springframework.web.bind.annotation.GetMapping;
 4 import org.springframework.web.bind.annotation.RequestMapping;
 5 import org.springframework.web.bind.annotation.RequestParam;
 6 import org.springframework.web.bind.annotation.RestController;
 7 
 8 import com.carry.annotation.LocalLock;
 9 
10 @RestController
11 @RequestMapping("/test")
12 public class LocalLockController {
13 
14     @LocalLock(key = "key:arg[0]")
15     @GetMapping
16     public String query(@RequestParam String token) {
17         return "success - " + token;
18     }
19 }

五、测试

启动项目,在postman中输入url:localhost:8080/test?token=1

第一次请求结果:

第二次请求结果:

 

posted @ 2018-08-06 11:03  CarryChan  阅读(4629)  评论(1编辑  收藏  举报