├── pom.xml
├── repeat-submit-intercept.iml
├── src
├── main
│ ├── java
│ │ └── com
│ │ │ └── gitee
│ │ │ └── taven
│ │ │ ├── ApiResult.java
│ │ │ ├── App.java
│ │ │ ├── aop
│ │ │ ├── NoRepeatSubmit.java
│ │ │ └── RepeatSubmitAspect.java
│ │ │ ├── controller
│ │ │ └── SubmitController.java
│ │ │ ├── test
│ │ │ └── RunTest.java
│ │ │ └── utils
│ │ │ ├── RedisLock.java
│ │ │ └── RequestUtils.java
│ └── resources
│ │ └── application.properties
└── test
│ └── java
│ └── com
│ └── gitee
│ └── taven
│ └── AppTests.java
└── target
├── classes
├── application.properties
└── com
│ └── gitee
│ └── taven
│ ├── ApiResult.class
│ ├── App.class
│ ├── aop
│ ├── NoRepeatSubmit.class
│ └── RepeatSubmitAspect.class
│ ├── controller
│ ├── SubmitController$UserBean.class
│ └── SubmitController.class
│ ├── test
│ └── RunTest.class
│ └── utils
│ ├── RedisLock.class
│ └── RequestUtils.class
└── test-classes
└── com
└── gitee
└── taven
└── AppTests.class
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.1.3.RELEASE
9 |
10 |
11 | com.gitee.taven
12 | repeat-submit-intercept
13 | 0.0.1-SNAPSHOT
14 | repeat-submit-intercept
15 | Demo project for Spring Boot
16 |
17 |
18 | 1.8
19 |
20 |
21 |
22 |
23 | org.springframework.boot
24 | spring-boot-starter-data-redis
25 |
26 |
27 | redis.clients
28 | jedis
29 |
30 |
31 | io.lettuce
32 | lettuce-core
33 |
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-web
39 |
40 |
41 | org.springframework.boot
42 | spring-boot-starter-aop
43 |
44 |
45 | org.springframework.boot
46 | spring-boot-devtools
47 | runtime
48 |
49 |
50 | org.springframework.boot
51 | spring-boot-starter-test
52 | test
53 |
54 |
55 |
56 | redis.clients
57 | jedis
58 |
59 |
60 |
61 | org.apache.commons
62 | commons-pool2
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | org.springframework.boot
71 | spring-boot-maven-plugin
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/repeat-submit-intercept.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/ApiResult.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven;
2 |
3 | public class ApiResult {
4 |
5 | private Integer code;
6 |
7 | private String message;
8 |
9 | private Object data;
10 |
11 | public ApiResult(Integer code, String message, Object data) {
12 | this.code = code;
13 | this.message = message;
14 | this.data = data;
15 | }
16 |
17 | public Integer getCode() {
18 | return code;
19 | }
20 |
21 | public void setCode(Integer code) {
22 | this.code = code;
23 | }
24 |
25 | public String getMessage() {
26 | return message;
27 | }
28 |
29 | public void setMessage(String message) {
30 | this.message = message == null ? null : message.trim();
31 | }
32 |
33 | public Object getData() {
34 | return data;
35 | }
36 |
37 | public void setData(Object data) {
38 | this.data = data;
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return "ApiResult{" +
44 | "code=" + code +
45 | ", message='" + message + '\'' +
46 | ", data=" + data +
47 | '}';
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/App.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.http.client.ClientHttpRequestFactory;
7 | import org.springframework.http.client.SimpleClientHttpRequestFactory;
8 | import org.springframework.web.client.RestTemplate;
9 |
10 | @SpringBootApplication
11 | public class App {
12 |
13 | public static void main(String[] args) {
14 | SpringApplication.run(App.class, args);
15 | }
16 |
17 | @Bean
18 | public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
19 | return new RestTemplate(factory);
20 | }
21 |
22 | @Bean
23 | public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
24 | SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
25 | return factory;
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/aop/NoRepeatSubmit.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.aop;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Target(ElementType.METHOD)
9 | @Retention(RetentionPolicy.RUNTIME)
10 | public @interface NoRepeatSubmit {
11 |
12 | /**
13 | * 设置请求锁定时间
14 | *
15 | * @return
16 | */
17 | int lockTime() default 10;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/aop/RepeatSubmitAspect.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.aop;
2 |
3 | import com.gitee.taven.ApiResult;
4 | import com.gitee.taven.utils.RedisLock;
5 | import com.gitee.taven.utils.RequestUtils;
6 | import org.aspectj.lang.ProceedingJoinPoint;
7 | import org.aspectj.lang.annotation.*;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.beans.factory.annotation.Autowired;
11 | import org.springframework.stereotype.Component;
12 | import org.springframework.util.Assert;
13 |
14 | import javax.servlet.http.HttpServletRequest;
15 | import java.util.UUID;
16 |
17 | @Aspect
18 | @Component
19 | public class RepeatSubmitAspect {
20 |
21 | private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
22 |
23 | @Autowired
24 | private RedisLock redisLock;
25 |
26 | @Pointcut("@annotation(noRepeatSubmit)")
27 | public void pointCut(NoRepeatSubmit noRepeatSubmit) {
28 | }
29 |
30 | @Around("pointCut(noRepeatSubmit)")
31 | public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
32 | int lockSeconds = noRepeatSubmit.lockTime();
33 |
34 | HttpServletRequest request = RequestUtils.getRequest();
35 | Assert.notNull(request, "request can not null");
36 |
37 | // 此处可以用token或者JSessionId
38 | String token = request.getHeader("Authorization");
39 | String path = request.getServletPath();
40 | String key = getKey(token, path);
41 | String clientId = getClientId();
42 |
43 | boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
44 | LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
45 |
46 | if (isSuccess) {
47 | LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
48 | // 获取锁成功
49 | Object result;
50 |
51 | try {
52 | // 执行进程
53 | result = pjp.proceed();
54 | } finally {
55 | // 解锁
56 | redisLock.releaseLock(key, clientId);
57 | LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
58 | }
59 |
60 | return result;
61 |
62 | } else {
63 | // 获取锁失败,认为是重复提交的请求
64 | LOGGER.info("tryLock fail, key = [{}]", key);
65 | return new ApiResult(200, "重复请求,请稍后再试", null);
66 | }
67 |
68 | }
69 |
70 | private String getKey(String token, String path) {
71 | return token + path;
72 | }
73 |
74 | private String getClientId() {
75 | return UUID.randomUUID().toString();
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/controller/SubmitController.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.controller;
2 |
3 | import com.gitee.taven.ApiResult;
4 | import com.gitee.taven.aop.NoRepeatSubmit;
5 | import org.springframework.web.bind.annotation.PostMapping;
6 | import org.springframework.web.bind.annotation.RequestBody;
7 | import org.springframework.web.bind.annotation.RestController;
8 |
9 | @RestController
10 | public class SubmitController {
11 |
12 | @PostMapping("submit")
13 | @NoRepeatSubmit(lockTime = 30)
14 | public Object submit(@RequestBody UserBean userBean) {
15 | try {
16 | // 模拟业务场景
17 | Thread.sleep(1500);
18 | } catch (InterruptedException e) {
19 | e.printStackTrace();
20 | }
21 |
22 | return new ApiResult(200, "成功", userBean.userId);
23 | }
24 |
25 | public static class UserBean {
26 | private String userId;
27 |
28 | public String getUserId() {
29 | return userId;
30 | }
31 |
32 | public void setUserId(String userId) {
33 | this.userId = userId == null ? null : userId.trim();
34 | }
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/test/RunTest.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.test;
2 |
3 | import com.gitee.taven.ApiResult;
4 | import com.gitee.taven.aop.RepeatSubmitAspect;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.boot.ApplicationArguments;
9 | import org.springframework.boot.ApplicationRunner;
10 | import org.springframework.http.HttpEntity;
11 | import org.springframework.http.HttpHeaders;
12 | import org.springframework.http.MediaType;
13 | import org.springframework.http.ResponseEntity;
14 | import org.springframework.stereotype.Component;
15 | import org.springframework.web.client.RestTemplate;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 | import java.util.concurrent.CountDownLatch;
20 | import java.util.concurrent.ExecutorService;
21 | import java.util.concurrent.Executors;
22 |
23 | @Component
24 | public class RunTest implements ApplicationRunner {
25 |
26 | private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);
27 |
28 | @Autowired
29 | private RestTemplate restTemplate;
30 |
31 | @Override
32 | public void run(ApplicationArguments args) throws Exception {
33 | System.out.println("执行多线程测试");
34 | String url="http://localhost:8000/submit";
35 | CountDownLatch countDownLatch = new CountDownLatch(1);
36 | ExecutorService executorService = Executors.newFixedThreadPool(10);
37 |
38 | for(int i=0; i<10; i++){
39 | String userId = "userId" + i;
40 | HttpEntity request = buildRequest(userId);
41 | executorService.submit(() -> {
42 | try {
43 | countDownLatch.await();
44 | System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());
45 | ResponseEntity response = restTemplate.postForEntity(url, request, String.class);
46 | System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());
47 |
48 | } catch (InterruptedException e) {
49 | e.printStackTrace();
50 | }
51 | });
52 | }
53 |
54 | countDownLatch.countDown();
55 | }
56 |
57 | private HttpEntity buildRequest(String userId) {
58 | HttpHeaders headers = new HttpHeaders();
59 | headers.setContentType(MediaType.APPLICATION_JSON);
60 | headers.set("Authorization", "yourToken");
61 | Map body = new HashMap<>();
62 | body.put("userId", userId);
63 | return new HttpEntity<>(body, headers);
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/utils/RedisLock.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.utils;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.data.redis.core.RedisCallback;
5 | import org.springframework.data.redis.core.StringRedisTemplate;
6 | import org.springframework.stereotype.Service;
7 | import redis.clients.jedis.Jedis;
8 |
9 | import java.util.Collections;
10 |
11 | /**
12 | * Redis 分布式锁实现
13 | * 如有疑问可参考 @see Redis分布式锁的正确实现方式
14 | *
15 | *
16 | */
17 | @Service
18 | public class RedisLock {
19 |
20 | private static final Long RELEASE_SUCCESS = 1L;
21 | private static final String LOCK_SUCCESS = "OK";
22 | private static final String SET_IF_NOT_EXIST = "NX";
23 | // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
24 | private static final String SET_WITH_EXPIRE_TIME = "EX";
25 | // if get(key) == value return del(key)
26 | private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
27 |
28 | @Autowired
29 | private StringRedisTemplate redisTemplate;
30 |
31 | /**
32 | * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
33 | * 对于 Redis 集群则无法使用
34 | *
35 | * 支持重复,线程安全
36 | *
37 | * @param lockKey 加锁键
38 | * @param clientId 加锁客户端唯一标识(采用UUID)
39 | * @param seconds 锁过期时间
40 | * @return
41 | */
42 | public boolean tryLock(String lockKey, String clientId, long seconds) {
43 | return redisTemplate.execute((RedisCallback) redisConnection -> {
44 | Jedis jedis = (Jedis) redisConnection.getNativeConnection();
45 | String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
46 | if (LOCK_SUCCESS.equals(result)) {
47 | return true;
48 | }
49 | return false;
50 | });
51 | }
52 |
53 | /**
54 | * 与 tryLock 相对应,用作释放锁
55 | *
56 | * @param lockKey
57 | * @param clientId
58 | * @return
59 | */
60 | public boolean releaseLock(String lockKey, String clientId) {
61 | return redisTemplate.execute((RedisCallback) redisConnection -> {
62 | Jedis jedis = (Jedis) redisConnection.getNativeConnection();
63 | Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
64 | Collections.singletonList(clientId));
65 | if (RELEASE_SUCCESS.equals(result)) {
66 | return true;
67 | }
68 | return false;
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/com/gitee/taven/utils/RequestUtils.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven.utils;
2 |
3 | import org.springframework.web.context.request.RequestContextHolder;
4 | import org.springframework.web.context.request.ServletRequestAttributes;
5 |
6 | import javax.servlet.http.HttpServletRequest;
7 |
8 | public class RequestUtils {
9 |
10 | public static HttpServletRequest getRequest() {
11 | ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
12 | return ra.getRequest();
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/src/main/resources/application.properties
--------------------------------------------------------------------------------
/src/test/java/com/gitee/taven/AppTests.java:
--------------------------------------------------------------------------------
1 | package com.gitee.taven;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 | import org.springframework.test.context.junit4.SpringRunner;
7 |
8 | @RunWith(SpringRunner.class)
9 | @SpringBootTest
10 | public class AppTests {
11 |
12 | @Test
13 | public void contextLoads() {
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/target/classes/application.properties:
--------------------------------------------------------------------------------
1 | server.port=8000
2 |
3 | # Redis���ݿ�������Ĭ��Ϊ0��
4 | spring.redis.database=0
5 | # Redis��������ַ
6 | spring.redis.host=localhost
7 | # Redis���������Ӷ˿�
8 | spring.redis.port=6379
9 | # Redis�������������루Ĭ��Ϊ�գ�
10 | #spring.redis.password=yourpwd
11 | # ���ӳ������������ʹ�ø�ֵ��ʾû�����ƣ�
12 | spring.redis.jedis.pool.max-active=8
13 | # ���ӳ���������ȴ�ʱ��
14 | spring.redis.jedis.pool.max-wait=-1ms
15 | # ���ӳ��е�����������
16 | spring.redis.jedis.pool.max-idle=8
17 | # ���ӳ��е���С��������
18 | spring.redis.jedis.pool.min-idle=0
19 | # ���ӳ�ʱʱ�䣨���룩
20 | spring.redis.timeout=5000ms
21 |
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/ApiResult.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/ApiResult.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/App.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/App.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/aop/NoRepeatSubmit.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/aop/NoRepeatSubmit.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/aop/RepeatSubmitAspect.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/aop/RepeatSubmitAspect.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/controller/SubmitController$UserBean.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/controller/SubmitController$UserBean.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/controller/SubmitController.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/controller/SubmitController.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/test/RunTest.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/test/RunTest.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/utils/RedisLock.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/utils/RedisLock.class
--------------------------------------------------------------------------------
/target/classes/com/gitee/taven/utils/RequestUtils.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/classes/com/gitee/taven/utils/RequestUtils.class
--------------------------------------------------------------------------------
/target/test-classes/com/gitee/taven/AppTests.class:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MissDistin/repeat-submit-intercept/fbd81ba6a85fc2baa69b9e353b2b811a54b9a4b7/target/test-classes/com/gitee/taven/AppTests.class
--------------------------------------------------------------------------------