介绍
在java程序中,对于controller层的新增、修改、提交操作,由于前端没有防抖错失,或者他人使用接口请求,短时间内会重复请求2次或多次,导致代码中插入2条重复的记录。
解决方案:1、接口增加锁,然后在代码中判断是否存在重复的数据,比如,名称已存在
2、将参数短时间内保存到redis中,如果出现了快速请求多次的情况,可以定义在几秒中内相同的参数,只允许提交一次,其他驳回,如果限制相同名称只能存在1条数据,还需要在代码中再增加查询数据库的判断。
代码实现
这里将核心的代码写在下面
防重复提交的切面类
package com.dragon7531.bdemo.aspect;
import com.alibaba.fastjson.JSON;
import com.dragon7531.bdemo.annotation.PreventRepeatSubmit;
import com.dragon7531.bdemo.exception.BusinessException;
import com.dragon7531.bdemo.utils.HttpCodeEnum;
import com.dragon7531.bdemo.utils.MD5Tools;
import com.dragon7531.bdemo.utils.RedisCache;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 第三版
* 注解中支持SpEL表达式,可以自己定义哪几个参数进行防重复提交
* 如果没有定义参数就按照所有的参数,作为防重复提交的参数
* 整理实现类的方法
*/
@Aspect
@Component
public class PreventRepeatSubmitAspect {
private static final Logger LOG = LoggerFactory.getLogger(PreventRepeatSubmitAspect.class);
private final SpelExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
// 令牌自定义标识
// @Value("${token.header}")
// private String header;
@Autowired
private RedisCache redisCache;
// 定义一个切入点
@Pointcut("@annotation(com.dragon7531.bdemo.annotation.PreventRepeatSubmit)")
public void preventRepeatSubmit() {
}
@Around("preventRepeatSubmit()")
public Object checkPrs(ProceedingJoinPoint pjp) throws Throwable {
LOG.info("进入preventRepeatSubmit切面");
//得到request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String requestURI = request.getRequestURI();
LOG.info("防重复提交的请求地址:{} ,请求方式:{}", requestURI, request.getMethod());
LOG.info("防重复提交拦截到的类名:{} ,方法:{}", pjp.getTarget().getClass().getSimpleName(), pjp.getSignature().getName());
MethodSignature ms = (MethodSignature) pjp.getSignature();
Method method = ms.getMethod();
PreventRepeatSubmit preventRepeatSubmit = method.getAnnotation(PreventRepeatSubmit.class);
String argStr = getArgStr(pjp, preventRepeatSubmit);
//这里替换是为了在redis可视化工具中方便查看
// argStr = argStr.replace(":", "#");
// MD5节省空间
argStr = MD5Tools.toMd5(argStr);
// 唯一值(没有消息头则使用请求地址)如果想限制一个用户重复提交可以使用这个token,防止所有人如果参数重复就不能提交这里就不能使用 token了
// String submitKey = request.getHeader(header);
// if (StringUtils.isEmpty(submitKey)) {
// submitKey = "1";
// } else {
// submitKey = submitKey.trim();
// }
// 这里全局防止参数重复提交
String submitKey = "1";
// 唯一标识(指定key + url +参数+token)
String cacheRepeatKey = "repeat_submit:" + requestURI + ":" + argStr + ":" + submitKey;
int interval = preventRepeatSubmit.interval();
LOG.info("获取到preventRepeatSubmit的有效期时间" + interval);
//redis分布式锁
Boolean aBoolean = redisCache.setNxCacheObject(cacheRepeatKey, 1, preventRepeatSubmit.interval(), TimeUnit.SECONDS);
//aBoolean为true则证明没有重复提交
if (!aBoolean) {
//JSON.toJSONString(ResponseResult.errorResult(HttpCodeEnum.SYSTEM_ERROR.getCode(),annotation.message())));
if (StringUtils.isEmpty(preventRepeatSubmit.message())) {
throw new BusinessException(HttpCodeEnum.REPEATE_ERROR);
} else {
throw new BusinessException(HttpCodeEnum.REPEATE_ERROR, preventRepeatSubmit.message());
}
}
return pjp.proceed();
}
/**
* 获取参数,可以是SpEL表达式中的key,也可以是所有参数
* @return 字符串
*/
public String getArgStr(ProceedingJoinPoint proceedingJoinPoint, PreventRepeatSubmit preventAnnotation) {
// key为空就获取所有参数
if (StringUtils.isEmpty(preventAnnotation.key())) {
Object[] args = proceedingJoinPoint.getArgs();
// 排除HttpServletRequest,HttpServletResponse参数
// 使用List来存储有效的参数
List<Object> validArgs = new ArrayList<>();
for (Object arg : args) {
if (!(arg instanceof HttpServletRequest || arg instanceof HttpServletResponse)) {
validArgs.add(arg);
}
}
String argStr = JSON.toJSONString(validArgs);
return argStr;
}
// 获取自定义的参数
return this.generateKeyBySpEL(preventAnnotation.key(), proceedingJoinPoint);
}
public String generateKeyBySpEL(String spELString, ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String[] paramNames = this.nameDiscoverer.getParameterNames(method);
if (paramNames == null) {
return "";
} else {
Expression expression = this.parser.parseExpression(spELString);
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; ++i) {
if (!(args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse)) {
context.setVariable(paramNames[i], args[i]);
}
}
Object value = expression.getValue(context);
return value == null ? "" : value.toString();
}
}
}
Redis工具类
package com.dragon7531.bdemo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
*
**/
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
//添加分布式锁
public <T> Boolean setNxCacheObject(final String key, final T value,long lt,TimeUnit tu)
{
return redisTemplate.opsForValue().setIfAbsent(key,value,lt,tu);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获取有效时间
*
* @param key Redis键
* @return 有效时间
*/
public long getExpire(final String key)
{
return redisTemplate.getExpire(key);
}
/**
* 判断 key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public Boolean hasKey(String key)
{
return redisTemplate.hasKey(key);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public boolean deleteObject(final Collection collection)
{
return redisTemplate.delete(collection) > 0;
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList)
{
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key)
{
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value)
{
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey)
{
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 删除Hash中的某条数据
*
* @param key Redis键
* @param hKey Hash键
* @return 是否成功
*/
public boolean deleteCacheMapValue(final String key, final String hKey)
{
return redisTemplate.opsForHash().delete(key, hKey) > 0;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
}
测试controller类
@RequestMapping("/insert2")
@PreventRepeatSubmit(interval = 40, key = "#student.name + '_'+ #student.age + '-' + #student.email", message = "学生有值了")
public ResponseResult insertStudent1(@Validated @RequestBody Student student, HttpServletResponse response, HttpServletRequest request) {
studentService.insert(student);
return ResponseResult.okResult();
}
测试结果:
{
"success": false,
"code": 500,
"msg": "学生有值了",
"data": null
}
学生表创建SQL
CREATE TABLE `t_student` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '学生ID',
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`email` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',
`phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号',
`id_card` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '身份证号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生信息表';
参考源码地址:
https://gitee.com/dragon7531/b-demo.git
视频讲解地址:
代码下载后启动不了
启动项目解决办法
将配置中心注释
展示完整的pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dragon7531</groupId>
<artifactId>b-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>b-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version> <!-- 或者更新版本 -->
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version> <!-- 或者更新版本 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>javax.validation</groupId>-->
<!-- <artifactId>validation-api</artifactId>-->
<!-- <version>2.0.1.Final</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.hibernate</groupId>-->
<!-- <artifactId>hibernate-validator</artifactId>-->
<!-- <version>6.0.17.Final</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.github.davidfantasy</groupId>-->
<!-- <artifactId>mybatis-plus-generator-ui</artifactId>-->
<!-- <version>2.0.1</version>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <artifactId>mybatis</artifactId>-->
<!-- <groupId>org.mybatis</groupId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--切面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.16.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<!-- es开始-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>6.4.3</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.4.3</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.4.3</version>
</dependency>
<!-- es结束-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.softlyl</groupId>
<artifactId>mybatis-print-sql</artifactId>
<version>1.0.5</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/mybatis-print-sql-1.0.5.jar</systemPath>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version> <!-- 版本号可根据需要调整 -->
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- Nacos 配置中心 -->
<!--<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>-->
</dependencies>
<!-- <dependencyManagement>-->
<!-- <dependencies>-->
<!-- <!– 引入 spring cloud 版本号 –>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-dependencies</artifactId>-->
<!-- <version>Greenwich.SR1</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
<!--<!– <dependency>–>-->
<!--<!– <groupId>com.alibaba.cloud</groupId>–>-->
<!--<!– <artifactId>spring-cloud-alibaba-dependencies</artifactId>–>-->
<!--<!– <version>2021.1</version> <!– 请根据实际发布的最新版本进行调整 –>–>-->
<!--<!– <type>pom</type>–>-->
<!--<!– <scope>import</scope>–>-->
<!--<!– </dependency>–>-->
<!-- </dependencies>-->
<!-- </dependencyManagement>-->
<profiles>
<profile>
<id>local</id>
<properties>
<envProfileActive>local</envProfileActive>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencies>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</profile>
<profile>
<id>test</id>
<properties>
<envProfileActive>test</envProfileActive>
</properties>
<dependencies>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
</profile>
</profiles>
<build>
<finalName>b-demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 把scope=system的依赖也打到jar中-->
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>application.properties</exclude>
<exclude>bootstrap.properties</exclude>
<exclude>application-local.properties</exclude>
<exclude>bootstrap-local.properties</exclude>
<exclude>application-test.properties</exclude>
<exclude>bootstrap-test.properties</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>application.properties</include>
<include>application-${envProfileActive}.properties</include>
</includes>
</resource>
</resources>
</build>
</project>
修改application-local.properties文件的数据库地址
修改地址和账户
spring.datasource.url=jdbc:mysql://localhost:3306/study?useUnicode=true&allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.dbb.url=jdbc:mysql://localhost:3306/study2?useUnicode=true&allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.dbb.username=root
spring.datasource.dbb.password=root
大家下载代码后,设置项目的maven地址和jdk
修改上面pom和数据库
复制我改动的PreventRepeatSubmitAspect类就可以了
9895

被折叠的 条评论
为什么被折叠?



