实现思路:首先定义一个拦截器,拦截所有请求,然后通过自定义注解在拦截器种判断该Controller的请求方法是否加上该注解,如果加上该注解,则执行拦截重复请求,如果第一次访问,则往redis新增一条记录,方法名和用户Token做为key,请求参数作为Value,如果再5秒Redis存在该请求则进行拦截并返回错误提示
Spring Boot项目需要引入的依赖
<!-- spring boot整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 对象池,使用redis时必须引入 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- hutool工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M2</version>
</dependency>
yml
配置
spring:
redis:
#Redis 主机地址
host: 106.75.148.160
#Redis 主机端口
port: 6389
password: as@15017170054AS
# 连接超时时间(记得添加单位,Duration)
timeout: 10000ms
# Redis默认情况下有16个分片,这里配置具体使用的分片
# database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1ms
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
如果你的Spring Boot项目已经整合好了Redis可以忽略上面两步
自定义去重提交注解
(1)我们首先定义一个注解,以后的controller中的接口如果需要防重复提交就加上这个注解:
注解中的value
代表我们防止重复提交的默认时间:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Repeat {
long value() default 10;
}
添加RedisUtil
工具类
https://songzixian.com/javatuils/599.html
添加ResponseResult
工具类
https://songzixian.com/javatuils/1084.html
CustomObjectMapper
类
/**
* <p>
* Description: TODO
* </p>
*
* @author songzixian
* @version v2.0.0
* @create 2022-04-03 19:27
* @see com.songzixian.mapper
*/
@Configuration
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
super();
//去掉默认的时间戳格式
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//设置为东八区
setTimeZone(TimeZone.getTimeZone("GMT+8"));
//设置日期转换yyyy-MM-dd HH:mm:ss
setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// 设置输入:禁止把POJO中值为null的字段映射到json字符串中
configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
// 空值不序列化
setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 反序列化时,属性不存在的兼容处理
getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 序列化枚举是以toString()来输出,默认false,即默认以name()来输出
configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
}
}
Constants
类
public class Constants {
// 统一的token前缀
public static final String TOKEN_PREFIX = "token:";
// 权限请求头
public static final String HEAD_AUTHORIZATION = "Authorization";
// token的过期时间
public static final Long TOKEN_TIME = 30*60L;
// 角色前缀
public static final String ROLE_PREFIX = "roles:";
// 权限的前缀
public static final String PERM_PREFIX = "perm:";
// 防止重复提交的key前缀
public static final String REPEAT_SUBMIT_KEY = "repeat:";
}
注册拦截器
/**
* <p>
* Description: TODO
* </p>
*
* @author songzixian
* @version v2.0.0
* @create 2022-04-03 14:13
* @see com.songzixian.config
*/
@Configuration
public class MyAppCofnig implements WebMvcConfigurer {
private final RepeatSubmitInterceptor repeatSubmitInterceptor;
public MyAppCofnig(RepeatSubmitInterceptor repeatSubmitInterceptor) {
this.repeatSubmitInterceptor = repeatSubmitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册自定义拦截器,添加拦截路径和排除拦截路径
registry.addInterceptor(repeatSubmitInterceptor) // 添加拦截器
.addPathPatterns("/**"); // 添加拦截拦截路径
WebMvcConfigurer.super.addInterceptors(registry);
}
}
RepeatSubmitInterceptor
实现类
import cn.hutool.json.JSONUtil;
import com.songzixian.constants.Constants;
import com.songzixian.interfaces.Repeat;
import com.songzixian.mapper.CustomObjectMapper;
import com.songzixian.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Map;
/**
* <p>
* Description: TODO
* </p>
*
* @author songzixian
* @version v2.0.0
* @create 2022-04-03 19:29
* @see com.songzixian.service
*/
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@Autowired
private CustomObjectMapper customObjectMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
// 获取我们的controller方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 判断是否有防止重复提交的注解
Repeat annotation = method.getAnnotation(Repeat.class);
if (annotation != null) {
if (this.isRepeatSubmit(request, annotation)) {
// 如果确定是重复提交,直接响应失败
// 自带格式
// ResponseEntity<String> body = ResponseEntity.status(500).body("您重复提交了当前的请求!");
// response.setStatus(500);
response.setContentType("application/json;charset=utf-8");
// 自定义格式
ResponseResult responseResult = new ResponseResult();
responseResult.setMessage("您重复提交了当前的请求");
responseResult.setCode(500);
response.getWriter().write(customObjectMapper.writeValueAsString(responseResult));
return false;
}
}
return true;
} else {
return true;
}
}
public boolean isRepeatSubmit(HttpServletRequest request, Repeat annotation) throws IOException {
// 获取参数列表,并序列化
Map<String, String[]> parameterMap = request.getParameterMap();
String nowParams = JSONUtil.toJsonStr(parameterMap);
System.out.println("请求参数--->" + nowParams);
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String token = request.getHeader(Constants.HEAD_AUTHORIZATION);
// 获得body的数据
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + token + ":" + url;
// 如果redis中没有存在这个key,说明这个请求是重复提交
String preParams = redisUtil.getString(cacheRepeatKey);
if (preParams != null && preParams.equals(nowParams)) {
return true;
}
// 否则我就存入redis,注意设置过期时间
redisUtil.setString(cacheRepeatKey, nowParams, annotation.value());
return false;
}
}
Controller测试类
注解通常可以使用在add,update这一类接口上,delete和query是没有必要的,因为这两种接口本身就是幂等性的。@Repeat(5)
//5表示提交的时间/ms
/**
* <p>
* Description: 宋子宪博客
* </p>
*
* @author songzixian
* @version v2.0.0
* @create 2022-04-03 19:18
* @see com.songzixian.controller
*/
@Slf4j
@RestController
public class TestController {
@Autowired
private RedisUtil redisUtil;
@Repeat(5) //加上@Repeat注解,表示该方法表单去重生效
@GetMapping("test01")
public String test01() {
log.info("test01方法被调用类...");
return "请求成功";
}
}
当第一次请求可以正常访问 ps:利用token作为Redis的ket来区分不同用户的请求,所以请记得加上Token测试
http://localhost:8080/test01?openId=orysv50bn7TEujnRQ6Tc9F-wlXdMist
请求成功后会完Redis插入一条记录5秒内相同的方法和参数再次重复请求,会出现一下效果