#redis
spring.redis.open=true
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=6000ms
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
1.8
3.0.6
1.2.10
5.1.8
2.2.6.RELEASE
2.0.4.RELEASE
1.3.0
1.0.28
8.0.11
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus-boot-starter}
org.springframework.data
spring-data-commons
${spring-data-commons.version}
org.springframework.boot
spring-boot-starter-data-jpa
${spring-boot-starter-data-jpa}
com.github.pagehelper
pagehelper-spring-boot-starter
${pagehelper-starter.version}
com.github.pagehelper
pagehelper
${pagehelper.version}
ch.qos.logback
logback-classic
1.2.3
org.projectlombok
lombok
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.spring.boot.version}
mysql
mysql-connector-java
${mysql.version}
com.alibaba
druid
${druid.version}
org.springframework.boot
spring-boot-starter-aop
org.springframework
spring-context-support
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
com.google.code.gson
gson
2.8.2
org.apache.commons
commons-pool2
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
io.lettuce
lettuce-core
com.alibaba
fastjson
1.2.45
log4j
log4j
1.2.17
org.slf4j
slf4j-api
1.7.25
org.slf4j
slf4j-log4j12
1.7.25
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 防止重复提交自定义注解
* @author zhang
*
*/
@Target(ElementType.METHOD) // 说明了Annotation所修饰的对象范围
@Retention(RetentionPolicy.RUNTIME) // 用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
@Documented // 是一个标记注解,没有成员 (文档化)
public @interface NoRepeatSubmit {
/**
* 设置请求锁定时间
*
* @return
*/
int lockTime() default 10;
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import com.zhang.common.annotation.NoRepeatSubmit;
import com.zhang.common.utils.R1;
import com.zhang.common.utils.RedisUtils;
import com.zhang.common.utils.RequestUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* 防止重复提交处理
* @author zhang
*
*/
@Aspect
@Component
public class RepeatSubmitAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
@Autowired
private RedisUtils redisUtils;
@Pointcut("@annotation(noRepeatSubmit)")
public void pointCut(NoRepeatSubmit noRepeatSubmit) {
}
@Around("pointCut(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
int lockSeconds = noRepeatSubmit.lockTime();
String threadName = Thread.currentThread().getName();// 获取当前线程名称
HttpServletRequest request = RequestUtils.getRequest();
Assert.notNull(request, "request can not null");
String token = request.getHeader("Authorization");// 前端调后台接口传 登录时已获取到的授权口令Authorization
String path = request.getServletPath();// 前端调接口地址
LOGGER.info("token;当前请求线程名称threadName;path:, token = [{}], threadName = [{}] , path = [{}]", token, threadName, path);
String key = getKey(token, path);// 调接口时生成临时key
String clientId = getClientId();// 调接口时生成临时value
// 此处为自测通过ProceedingJoinPoint获取当前请求的方法名称
// 方法一
Signature signature = pjp.getSignature();
MethodSignature ms = (MethodSignature)signature;
Method method = ms.getMethod();
LOGGER.info("获取方法一, method = [{}]", method);
// 方法二
Signature sig = pjp.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("该注解只能用于方法");
}
msig = (MethodSignature) sig;
Object target = pjp.getTarget();
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
LOGGER.info("获取方法二, currentMethod = [{}]", currentMethod);
boolean isSuccess = redisUtils.tryLock(key, clientId, lockSeconds);// 用于添加锁,如果添加成功返回true,失败返回false
LOGGER.info("tryLock 当前请求线程名称threadName, threadName = [{}], key = [{}], clientId = [{}]", threadName, key, clientId);
if (isSuccess) {
LOGGER.info("tryLock success 线程名称threadName:, threadName = [{}], key = [{}], clientId = [{}]",threadName, key, clientId);
// 获取锁成功
Object result;
try {
// 执行进程
result = pjp.proceed();// aop代理链执行的方法
} finally {
// 据key从redis中获取value
String value = redisUtils.get(key);
LOGGER.info("releaseLock start when 'value = clientId', key = [{}], value = [{}], clientId = [{}]", key, value, clientId);
if (clientId.equals(redisUtils.get(key))) {
LOGGER.info("releaseLock start, key = [{}], value = [{}], clientId = [{}]", key, value, clientId);
// 解锁
redisUtils.releaseLock(key, clientId);
LOGGER.info("releaseLock success 线程名称threadName:, threadName = [{}], key = [{}], clientId = [{}]",threadName, key, clientId);
}
}
return result;
} else {
// 添加锁失败,认为是重复提交的请求
LOGGER.info("重复请求,请稍后再试 线程名称threadName:" + threadName);
LOGGER.info("tryLock fail 线程名称threadName:, threadName = [{}], key = [{}]",threadName, key);
return R1.error(403, "重复请求,请稍后再试");
}
}
private String getKey(String token, String path) {
return token + path;
}
private String getClientId() {
return UUID.randomUUID().toString();
}
}
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* Redis 工具类
* @author zhang
* @date 2020-04-24 10:28:40
*/
@Component
public class RedisUtils {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
// private static final String LOCK_SUCCESS = "OK";
private static final Long LOCK_SUCCESS1 = 1L;
private static final String SET_IF_NOT_EXIST = "NX";
/** 设置过期时间单位, EX = seconds; PX = milliseconds */
private static final String SET_WITH_EXPIRE_TIME = "EX";
// if get(key) == value return del(key)
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";
/** 不设置过期时长 */
public final static long NOT_EXPIRE = -1;
/** 默认过期时长,单位:秒 */
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
Gson gson = new Gson();
public void set(String key, Object value, long expire){
stringRedisTemplate.opsForValue().set(key, toJson(value));
if(expire != NOT_EXPIRE){
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
}
public void set(String key, Object value){
set(key, value, DEFAULT_EXPIRE);
}
public T get(String key, Class clazz, long expire) {
String value = stringRedisTemplate.opsForValue().get(key);
if(expire != NOT_EXPIRE){
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value == null ? null : fromJson(value, clazz);
}
public T get(String key, Class clazz) {
return get(key, clazz, NOT_EXPIRE);
}
public String get(String key, long expire) {
String value = stringRedisTemplate.opsForValue().get(key);
if(expire != NOT_EXPIRE){
stringRedisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
return value;
}
public String get(String key) {
return get(key, NOT_EXPIRE);
}
public void delete(String key) {
stringRedisTemplate.delete(key);
}
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public boolean tryLock(String lockKey, String clientId, int seconds) {
return stringRedisTemplate.execute((RedisCallback) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
//此方法报错,查看源码jedis.set方法不能如此传参
// String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
// SetParams setParams = new SetParams();// 设置key的过期时间单位
// setParams.px(seconds); // (EX = seconds; PX = milliseconds)
// String result = jedis.set(lockKey, clientId, setParams);
long result = jedis.setnx(lockKey, clientId);// jedis.setnx该方式赋值(成功返回1,否则返回0)
if (LOCK_SUCCESS1 == result) {
return true;
}
return false;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public boolean releaseLock(String lockKey, String clientId) {
return stringRedisTemplate.execute((RedisCallback) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
});
}
/**
* Object转成JSON数据(Gson方式)
*/
private String toJson(Object object){
if(object instanceof Integer || object instanceof Long || object instanceof Float ||
object instanceof Double || object instanceof Boolean || object instanceof String){
return String.valueOf(object);
}
return gson.toJson(object);
}
/**
* JSON数据转成Object(Gson方式)
*/
private T fromJson(String json, Class clazz){
return gson.fromJson(json, clazz);
}
}
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class RequestUtils {
public static HttpServletRequest getRequest() {
ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return ra.getRequest();
}
}
import java.util.ArrayList;
import java.util.Map;
import org.apache.http.HttpStatus;
/**
* 接口统一返回格式
* @author zhang
*
*/
public class R1 {
private static final long serialVersionUID = 1L;
private int code;
private T data;
private String message;
/**
* 无参构造
*/
public R1() {
this.code = 0;
this.message = "success";
this.data = (T) new ArrayList
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zhang.common.annotation.NoRepeatSubmit;
import com.zhang.common.utils.R1;
/**
* 测试noReatSubmit
* @author zhang
*
*/
@RestController
@RequestMapping("/noReatSubmit")
public class NoReatSubmitController {
@PostMapping("submit")
@NoRepeatSubmit(lockTime = 30)
public R1> submit(@RequestBody UserBean userBean) {
try {
// 模拟业务场景
System.out.println("模拟业务场景开始...,当前请求线程名:" + userBean.getThreadName());
Thread.sleep(1500);
} catch (InterruptedException e) {
System.out.println("模拟业务场景失败");
e.printStackTrace();
}
System.out.println("模拟业务场景成功,当前请求线程名称:" + userBean.getThreadName());
return new R1(200, userBean.getUserId(), "成功");
}
public static class UserBean {
private String userId;
private String threadName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId == null ? null : userId.trim();
}
public String getThreadName() {
return threadName;
}
public void setThreadName(String threadName) {
this.threadName = threadName;
}
@Override
public String toString() {
return "UserBean [userId=" + userId + ", threadName=" + threadName + "]";
}
}
}
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
/**
* http请求方式
* @author zhang
*
*/
public class HttpClientUtil {
private static RequestConfig requestConfig;
private static SSLConnectionSocketFactory sslsf;
static {
SSLContext sslContext = null;
try {
sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
// 信任所有证书 https://blog.csdn.net/sleeping_/article/details/50500351
@Override
public boolean isTrusted(java.security.cert.X509Certificate[] arg0, String arg1)
throws java.security.cert.CertificateException {
return true;
}
}).build();
} catch (Exception e) {
e.printStackTrace();
}
sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
}
/**
* get方式請求(有参)
* @param url
* @param param
* @param Authorization 口令授权
* @return
*/
public static String doGet(String url, Map param, String Authorization, String webAuthorization) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建uri
URIBuilder builder = new URIBuilder(url);
if (param != null) {
for (String key : param.keySet()) {
if (param.get(key).getClass().isArray()) {
String[] arr = (String[]) param.get(key);
for (String string : arr) {
builder.addParameter(key, string);
}
}else{
builder.addParameter(key, param.get(key).toString());
}
}
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
httpGet.addHeader("Connection", "close");
httpGet.addHeader("Authorization", Authorization);
httpGet.addHeader("Web-Authorization", webAuthorization);
httpGet.setConfig(requestConfig);
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* get方式请求(无参)
* @param url
* @param Authorization 口令授权
* @return
*/
public static String doGet(String url, String Authorization, String webAuthorization) {
return doGet(url, null, Authorization, webAuthorization);
}
/**
* post方式请求(有参)
* @param url
* @param param
* @param Authorization 口令授权
* @return
*/
public static String doPost(String url, Map param, String Authorization, String webAuthorization) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Connection", "close");
httpPost.addHeader("Authorization", Authorization);
httpPost.addHeader("Web-Authorization", webAuthorization);
httpPost.setConfig(requestConfig);
// 创建参数列表
if (param != null) {
List paramList = new ArrayList<>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, "utf-8");
httpPost.setEntity(entity);
}
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* post方式请求(无参)
* @param url
* @param Authorization 口令授权
* @return
*/
public static String doPost(String url, String Authorization, String webAuthorization) {
return doPost(url, null, Authorization, webAuthorization);
}
/**
* delete方式请求(有参)
* @param url
* @param param
* @param Authorization 口令授权
* @return
*/
public static String doDelete(String url, String Authorization, String webAuthorization) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
String resultString = "";
CloseableHttpResponse response = null;
try {
// 创建http delete请求
HttpDelete httpDelete = new HttpDelete(url);
httpDelete.addHeader("Connection", "close");
httpDelete.addHeader("Authorization", Authorization);
httpDelete.addHeader("Web-Authorization", webAuthorization);
httpDelete.setConfig(requestConfig);
// 执行请求
response = httpclient.execute(httpDelete);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* put方式请求(有参)
* @param url
* @param param
* @param Authorization 口令授权
* @return
*/
public static String doPut(String url, String json, String Authorization, String webAuthorization) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPut httpPut = new HttpPut(url);
httpPut.addHeader("Connection", "close");
httpPut.addHeader("Authorization", Authorization);
httpPut.addHeader("Web-Authorization", webAuthorization);
httpPut.setConfig(requestConfig);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPut.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPut);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 请求的参数类型为json
* post方式请求 (有参)
* @param url
* @param json
* @param Authorization 口令授权
* @return
*/
public static String doPostJson(String url, String json, String Authorization, String webAuthorization) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
System.out.println("创建Http Post请求");
httpPost.addHeader("Connection", "close");
httpPost.addHeader("Authorization", Authorization);
httpPost.addHeader("Web-Authorization", webAuthorization);
httpPost.setConfig(requestConfig);
// 创建请求内容
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
System.out.println("请求内容:" + entity.toString());
httpPost.setEntity(entity);
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
}
import com.alibaba.fastjson.JSONObject;
import com.zhang.common.utils.HttpClientUtil;
import com.zhang.modules.test.controller.NoReatSubmitController.UserBean;
/**
* 创建线程测试接口
* @author zhang
*
*/
public class RunnableDemo implements Runnable {
private Thread t;
private String threadName;
public RunnableDemo(String name) {
threadName = name;
System.out.println("Creating: " + threadName);
}
public String threadName() {
return threadName;
}
public void run() {
System.out.println("Running, threadName = [{}]: " + threadName);
UserBean userBean = new UserBean();
userBean.setUserId("123123123");
userBean.setThreadName(threadName);
String url="http://localhost:9999/zhang/noReatSubmit/submit";
HttpClientUtil.doPostJson(url, JSONObject.toJSONString(userBean), "youToken", null);
System.out.println("Thread " + threadName + " exiting.");
}
public void start() {
System.out.println("Starting " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.zhang.modules.test.controller.RunnableDemo;
@SpringBootApplication
@ComponentScan(basePackages={"com.zhang"})
public class TestProjectApplication {
public static void main(String[] args) {
SpringApplication.run(TestProjectApplication.class, args);
System.out.println("启动成功!");
// 开启线程
RunnableDemo R1 = new RunnableDemo( "Thread-1");
RunnableDemo R2 = new RunnableDemo( "Thread-2");
// RunnableDemo R3 = new RunnableDemo( "Thread-3");
// RunnableDemo R4 = new RunnableDemo( "Thread-4");
R1.start();
R2.start();
// R3.start();
// R4.start();
}
}
https://www.toutiao.com/i6768274855004471821/?tt_from=weixin&utm_campaign=client_share&wxshare_count=1&from=groupmessage×tamp=1582519759&app=news_article&utm_source=weixin&utm_medium=toutiao_android&req_id=202002241249180101310981964407620D&group_id=6768274855004471821 —— Spring Boot 使用 AOP 防止重复提交
https://www.cnblogs.com/peida/archive/2013/04/24/3036689.html —— 深入理解Java:注解(Annotation)自定义注解入门
https://www.cnblogs.com/linjiqin/p/8003838.html —— Redis分布式锁的正确实现方式
http://redisdoc.com/ —— Redis 命令参考