防重复提交流程
获取到当前的 httpservletrequest 对象,并记录请求的地址、请求方式、拦截到的类名和方法名等信息。
通过 pjp.getargs() 获取请求参数,并将参数转换成字符串,用于生成唯一标识。
根据请求的地址、参数、唯一标识等信息生成缓存键 cacherepeatkey,用于作为重复提交判断的依据。
通过 pjp.getsignature().getmethod() 和 method.getannotation(preventrepeatsubmit.class) 获取被拦截的方法上的 preventrepeatsubmit 注解,进而获取注解中配置的有效期时间。
使用 redis 分布式锁来判断请求是否重复提交。调用 rediscache.setnxcacheobject() 方法,尝试向缓存中设置键值对,如果设置成功(返回值为 true),则证明没有重复提交。若设置失败(返回值为 false),则抛出 businessexception 异常,表示重复提交。
如果没有重复提交,则执行目标方法,即 pjp.proceed(),并将其返回。

引入依赖
<!--切面-->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-aop</artifactid>
</dependency>
<dependency>
<groupid>com.baomidou</groupid>
<artifactid>mybatis-plus-boot-starter</artifactid>
<version>3.5.1</version>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>
<!--rabbitmq 测试依赖-->
<dependency>
<groupid>org.springframework.amqp</groupid>
<artifactid>spring-rabbit-test</artifactid>
<scope>test</scope>
</dependency>
<!-- 数据库-->
<dependency>
<groupid>mysql</groupid>
<artifactid>mysql-connector-java</artifactid>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>fastjson</artifactid>
<version>1.2.47</version>
</dependency>properties配置
server.port=7125 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/itcast?servertimezone=gmt%2b8&useunicode=true&characterencoding=utf8&autoreconnect=true&allowmultiqueries=true spring.datasource.username=root spring.datasource.password=root123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.driver spring.datasource.hikari.pool-name=hikaricpdatasource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=180000 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.auto-commit=true spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.connection-test-query=select 1 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.timeout=10s spring.redis.password=123 token.header=token
自定义注解
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
*
*/
@inherited
@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface preventrepeatsubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 40;
/**
* 提示消息
*/
public string message() default "不允许重复提交,请稍候再试";
}
切面
import com.alibaba.fastjson.json;
import com.example.demo.annotation.preventrepeatsubmit;
import com.example.demo.exception.businessexception;
import com.example.demo.util.httpcodeenum;
import com.example.demo.util.rediscache;
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.stereotype.component;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;
import javax.servlet.http.httpservletrequest;
import java.lang.reflect.method;
import java.util.concurrent.timeunit;
@aspect
@component
public class preventrepeatsubmitaspect {
private static final logger log = loggerfactory.getlogger(preventrepeatsubmitaspect.class);
// 令牌自定义标识
@value("${token.header}")
private string header;
@autowired
private rediscache rediscache;
// 定义一个切入点
@pointcut("@annotation(com.example.demo.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());
//获取请求参数
object[] args = pjp.getargs();
string argstr = json.tojsonstring(args);
//这里替换是为了在redis可视化工具中方便查看
argstr=argstr.replace(":","#");
// 唯一值(没有消息头则使用请求地址)
string submitkey = request.getheader(header).trim();
// 唯一标识(指定key + url +参数+token)
string cacherepeatkey = "repeat_submit:" + requesturi+":" +argstr+":"+ submitkey;
methodsignature ms = (methodsignature) pjp.getsignature();
method method=ms.getmethod();
preventrepeatsubmit preventrepeatsubmit=method.getannotation(preventrepeatsubmit.class);
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())));
throw new businessexception(httpcodeenum.repeate_error);
}
return pjp.proceed();
}
}
redis工具类
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
import com.example.demo.annotation.preventrepeatsubmit;
import com.example.demo.mapper.stumapper;
import com.example.demo.model.responseresult;
import com.example.demo.model.student;
import org.springframework.validation.annotation.validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.resource;
@restcontroller
@requestmapping("/test")
@validated
public class testcontroller {
@resource
private stumapper stumapper;
@postmapping("/user")
@preventrepeatsubmit
public responseresult<string> user(@requestbody student student) {
stumapper.insert(student);
return new responseresult<>("插入成功");
}
// @postmapping("/user/{name}/{age}")
// @preventrepeatsubmit
// public responseresult<string> user( @pathvariable string name ,@pathvariable integer age) {
// student student = new student(name,age);
// stumapper.insert(student);
// return new responseresult<>("插入成功");
// }
}测试
发送请求的请求头

请求体

第一次插入

第二次插入失败

redis缓存

改用restful风格

第一次请求

结果

第二次发起请求失败

总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论