程序员博客

分享编程经验与技术教程

0%

「Java 开发实例」SpringBoot+AOP+注解+Redis防重复提交(防抖)

🙏废话不多说系列,直接开整🙏

cat008.png

前期知识储备:

  1. 什么是防抖?
  • ① 控制用户频繁快速点击同一个操作;
  • ② 防止网络抖动;
  • ③ 避免接口请求频繁过载;

  1. 哪一类接口需要防抖?
  • 用户输入类接口;
  • 按钮点击类接口;
  • 滚动加载类接口;

  1. 防抖的大致思路是?
  • 防抖即 重复提交,接口参数再短时间一致,如果多台机器客户端发送过来的请求被拦截了也是不合理的,需要再加上 IP地址进行区分。

一、前提准备

(1)Maven 引入
1
2
3
4
5
6
7
8
9
10
11
<!-- spring boot aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring boot redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

(2)配置文件
1
2
3
4
5
6
7
8
9
10
# 应用名称
server.port=39002
spring.application.name=transfer
# redis config
spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379
spring.redis.password=
spring.redis.timeout=2

二、完整源码

核心功能:① 通过注解的方式指定接口的重复时间;② 可以指定接口锁定日期和时间粒度;

(1)定义注解 @NoRepeatSubmitAopByRedis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
* 定义一个注解(目前可以指定缓存KEY的时间)
*
* @author drew
* @apiNote @Target(ElementType.METHOD) 作用到方法上
* @apiNote @Retention(RetentionPolicy.RUNTIME) 只有运行时有效
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmitByRedis {
/**
* 锁定时间
* @return 锁定时间
*/
int lockedTime() default 2;

/**
* 时间单位(时分秒等)
* @return 单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}

(2)定义切面 NoRepeatSubmitByRedisAop.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import edu.study.module.aop.utils.ApiResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

/**
* @author zl
* @create 2021-04-03 9:28
*/
@Aspect
@Configuration
public class NoRepeatSubmitByRedisAop {
private final Log logger = LogFactory.getLog(getClass());

@Resource
private RedisTemplate redisTemplate;

@Pointcut("@annotation(noRepeatSubmitByRedis)")
public void pointCut(NoRepeatSubmitByRedis noRepeatSubmitByRedis) {
}

@Before("@annotation(noRepeatSubmitByRedis)")
public void before(NoRepeatSubmitByRedis noRepeatSubmitByRedis) {
}

@Around("pointCut(noRepeatSubmitByRedis)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmitByRedis noRepeatSubmitByRedis) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String sessionId = Objects.requireNonNull(RequestContextHolder.getRequestAttributes()).getSessionId();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
String key = sessionId + "-" + request.getServletPath();
// 如果缓存中有这个url视为重复提交
if (redisTemplate.opsForValue().get(key) == null) {
Object o = pjp.proceed();
redisTemplate.opsForValue().set(key, request.getRequestURI());
logger.info("请求的KEY:" + key + ",请求URI:" + request.getRequestURI());
redisTemplate.expire(key, noRepeatSubmitByRedis.lockedTime(), noRepeatSubmitByRedis.timeUnit());
return o;
} else {
logger.error("重复提交");
return new ApiResult(888, "请勿短时间内重复操作", null);
}
} catch (Throwable e) {
e.printStackTrace();
logger.error("验证重复提交时出现未知异常!");
return new ApiResult(889, "验证重复提交时出现未知异常!", null);
}
}

}

三、测试演示

(1)控制层 TestNoRepeatSubmit.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import edu.study.module.aop.NoRepeatSubmit;
import edu.study.module.aop.NoRepeatSubmitByRedis;
import edu.study.module.aop.utils.ApiResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping(path = "/repeatSubmit")
public class TestRepeatSubmitController {

/**
* redis: 添加防重复提交注解(提供指定时间和时间粒度)
*/
@NoRepeatSubmitByRedis(lockedTime = 8, timeUnit = TimeUnit.SECONDS)
@RequestMapping("/submitByRedis")
public ApiResult testByRedis() {
return new ApiResult(0, "测试通过", null);
}

}

(2)浏览器请求地址

http://localhost:39002/repeatSubmit/submitByRedis

image.png

(3)控制台输出

image.png

四、总结

(1)此切面没有考虑到  自身如果出现异常,那么如何处理已经保存的缓存KEY呢?需要添加注解 @AfterThrowing 以及 @AfterReturning 注解等相关的方法;

附录

相关具体源码见:【github.com/GitSuperDre…

(1)统一返回类 ApiResult.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class ApiResult {

private Integer code;
private String message;
private Object data;

public ApiResult(Integer code, String message, Object data) {
this.code = code;
this.message = message;
this.data = data;
}

public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
public String getMessage() {return message;}
public void setMessage(String message) {this.message = message == null ? null : message.trim();}
public Object getData() {return data;}
public void setData(Object data) {this.data = data;}

@Override
public String toString() {
return "ApiResult{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}

}


🙏至此,非常感谢阅读🙏

cat008.png

原文链接: https://juejin.cn/post/7365831823928229938