接口防抖
接口防抖主要用于控制在短时间内多次触发的事件或请求,确保在一定时间内只执行一次操作。这种技术可以应用于前端和后端,以优化性能和用户体验,减少不必要的计算或网络请求。
虽然说接口请求可以直接在前端实现,但是为了防止有用户恶意绕过前端限制,直接通过工具或脚本来发起请求。本着不相信任何人的原则,还是在后端也做防抖合适。
redis实现
思路
实现一个拦截器,在请求到达控制器之前拦截。然后判断redis中有没有数据,如果有 说明指定时间内已经调用过了,拦截,如果没有,说明是正常请求,在redis中添加一条数据并设置过期时间,表示指定时间内不能再请求了
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
private static final long DEBOUNCE_TIME = 20; // 防抖时间(秒)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String key = "repeat-submit:" + request.getSession().getId() + ":" + request.getRequestURI();
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return false; // 拦截请求
}
// 设置防抖时间 (秒)
redisTemplate.opsForValue().set(key, "1", DEBOUNCE_TIME, TimeUnit.SECONDS);
return true; // 继续执行请求
}
}
注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
}
addPathPatterns(“/**”) 表示拦截所有的方法,若需要拦截指定的方法增加addPathPatterns(“方法路径”)即可
注解+AOP实现
通过在方法上增加注解的方式,实现指定接口的防抖
思路
通过AOP的前置通知来拦截标记了
RepeatSubmit
注解的方法。在方法执行前,检查Redis中是否存在锁。如果存在锁,则返回提示信息。如果不存在锁,则在Redis中设置锁,并继续执行方法。
定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/* 防抖时间 */
int time() default 1000; // 默认间隔时间,单位毫秒
}
切面
@Aspect
@Component
public class RepeatSubmitAspect {
@Resource
private StringRedisTemplate stringRedisTemplate;
/* 只增强带RepeatSubmit注解的方法 */
@Before("@annotation(com.example.demo.antishake.annotation.RepeatSubmit)")
public void interceptor(JoinPoint joinPoint) throws Throwable {
// 获取HttpServletRequest对象
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
// 获取方法签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 获取方法
Method method = methodSignature.getMethod();
// 获取方法上的注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 用调用方法的路径+用户的token作为key
String key = "repeat-submit:"+ request.getRequestURI()+request.getHeader("Authorization");
// 判断缓存中是否存在该key, 存在则说明已经提交过该请求
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(key))) {
throw new RuntimeException("请勿重复提交");
}
if (annotation != null) {
// 获取注解参数
int time = annotation.time();
stringRedisTemplate.opsForValue().set(key, "1", time, TimeUnit.MILLISECONDS);
}
}
}
测试
@RestController
public class TestController {
@RepeatSubmit(time = 10000)
@GetMapping("/test")
public String test(){
return "请求成功";
}
@GetMapping("/test01")
public String test01(){
return "请求成功01";
}
}
测试test接口,防抖时间是10秒,连续请求就会失败
注意:上面当作锁的key,可能不太合理,根据具体场景修改,如提交的参数。