├── 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 --------------------------------------------------------------------------------