掘金 后端 ( ) • 2024-05-07 10:41

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

cat008.png

一、前提准备

(1)Maven 引入
<!-- 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)配置文件
# 应用名称
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
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
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
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 注解等相关的方法;

附录

相关具体源码见:【https://github.com/GitSuperDrew/SpringBootDemo/springboot-api-lock

(1)统一返回类 ApiResult.java
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