一般遇见这种需求,大体思路思路我想基本是这样的,

1.自定义一个spring-boot-starter

2.启动一个拦截器实现拦截自定义注解

3.根据注解的一些属性进行拼接一个key

4.判断key是否存在

4.1 不存在 存入redis,然后设置一个过期时间(一般过期时间也是注解的一个属性)

4.2 存在则抛出一个重复提交异常

 

闲话少说,先来一个使用端代码以及结果

使用方式

springboot 字段重复校验注解 springboot防止重复提交注解_springboot 字段重复校验注解

key = "T(cn.goswan.orient.common.security.util.SecurityUtils).getUser().getUsername()+#test.id"

这部分 的key就是拦截器里面用到的判断的key,具体可以根据自己业务用el表达式去定义

我用的是class fullpanth+用户名+业务主键 当作判定key

expireTime = 3

设置为了 3 

timeUnit = TimeUnit.SECONDS

设置为了秒,即为3秒后这个key从缓存中消失,使用端一定注意这个时常一定要大于自己的业务处理耗时

好了下面上结果,连续发送两次请求(postman 发送)第一次请求并没有报错

第二次请求抛出如下错误(自定义的错误)

exception.IdempotentException: classUrl public cn.goswan.orient.common.core.util.R com..demo.controller.TestController.save(com.demo.entity.Test) not allow repeat submit

好了,说了这么多,下面上源码


目录结构

springboot 字段重复校验注解 springboot防止重复提交注解_redis_02


pom 文件(这里的comm-data实际上内部是对redis 的引用配置可以忽略,大家可以替换成自己的redis 配置即可,如果有不明白的可以看看我之前的文件,redis templete 哨兵配置代码参考一下)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.goswan</groupId>
        <artifactId>orient-common</artifactId>
        <version>3.9.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>basal-common-idempotent</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.goswan</groupId>
            <artifactId>orient-common-data</artifactId>
        </dependency>
    </dependencies>

</project>

Idempotent.java


package com.basal.common.idempotent.annotation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @Author alan.wang
 * @date: 2021-12-30 17:54
 * @desc: 定义注解
 */

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {

    /**
     * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
     * @return Spring-EL expression
     */
    String key() default "";


//    /**
//     * 是否作用域是所有请求(根据请求ip)
//     * 默认:false
//     *  false:只做用在当前请求人(限定同意时间段只对当前访问ip拦截)
//     *  ture:  作用在所有人(同一时间对所有ip进行拦截)
//     *
//     * @return isWorkOnAll
//     **/
//    boolean isWorkOnAll() default false;

    /**
     * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
     * @return expireTime
     */
    int expireTime() default 1;

    /**
     * 时间单位 默认:s
     * @return TimeUnit
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}

IdempotentAspect.java


package com.basal.common.idempotent.aspect;

import cn.goswan.orient.common.data.util.StringUtils;
import com.basal.common.idempotent.annotation.Idempotent;
import com.basal.common.idempotent.exception.IdempotentException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.Redisson;
import org.redisson.api.RMapCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Objects;

/**
 * @Author alan.wang
 * @date: 2021-12-30 17:56
 * @desc:
 * 防止重复提交注解拦截器,具体流程就是拦截带@Idempotent的方法,然后从redis取出key
 * 如果key 已经存在:抛出自定义异常
 * 如果key不存在:则存入
 */
@Aspect
public class IdempotentAspect {

    final SpelExpressionParser PARSER = new SpelExpressionParser();

    final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    private static final String RMAPCACHE_KEY = "idempotent";

    @Autowired
    private Redisson redisson;

    @Pointcut("@annotation(com.basal.common.idempotent.annotation.Idempotent)")
    public void pointCut() {
    }

    @Before("pointCut()")
    public void beforeCut(JoinPoint joinPoint) {


        //获取切面拦截的方法
        Object[] arguments = joinPoint.getArgs();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        if (!methodSignature.getMethod().isAnnotationPresent(Idempotent.class)) {
            return;
        }
        Method method = ((MethodSignature) signature).getMethod();
        if (method.getDeclaringClass().isInterface()) {
            try {
                method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
                        method.getParameterTypes());
            } catch (SecurityException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }

        //获取切面拦截的方法的参数并放入值context中
        StandardEvaluationContext context = new StandardEvaluationContext();
        String[] params = DISCOVERER.getParameterNames(method);
        if (params != null && params.length > 0) {
            for (int len = 0; len < params.length; len++) {
                context.setVariable(params[len], arguments[len]);
            }
        }

        //获取类全路径作为根key
        String classUrl = method.toString();
        Idempotent idempotent = methodSignature.getMethod().getAnnotation(Idempotent.class);
        String idKey = "";
        if (StringUtils.isEmpty(idempotent.key())) {
            idKey = classUrl;
        } else {

            //将annotation中的key 获取到并通过spelExpression 转为具体值
            SpelExpression spelExpression = PARSER.parseRaw(idempotent.key());
            String key = spelExpression.getValue(context, String.class);
            idKey = classUrl + key;
        }

        //判断map 中是否已经存在key
        RMapCache rMapCache = redisson.getMapCache(RMAPCACHE_KEY);

        //存在则抛出重复提交异常
        if (rMapCache.containsKey(idKey)) {

            throw new IdempotentException("classUrl " + classUrl + " not allow repeat submit ");
        } else {

            //不存在则存入cache map,如果存入过程中又有操作以至于存在key,则同样抛出异常
            Object idObj = rMapCache.putIfAbsent(idKey, System.currentTimeMillis(), idempotent.expireTime(), idempotent.timeUnit());
            if (Objects.nonNull(idObj)) {
                throw new IdempotentException("classUrl " + classUrl  + " not allow repeat submit ");
            }
        }


    }
}

IdempotentConfig.java


package com.basal.common.idempotent.config;

import com.basal.common.idempotent.aspect.IdempotentAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author alan.wang
 * @date: 2022-01-03 11:35
 * @desc: 将IdempotentAspect 拦截器注入到spring 容器中
 */
@Configuration
public class IdempotentConfig {


    @Bean
    public IdempotentAspect IdempotentAspect(){
        IdempotentAspect idempotentAspect = new IdempotentAspect();
        return idempotentAspect;
    }
}


IdempotentException.java


package com.basal.common.idempotent.exception;

/**
 * @Author alan.wang
 * @date: 2022-01-04 15:26
 * @desc: Idempotent 重复提交异常
 */
public class IdempotentException extends RuntimeException {

    public IdempotentException() {
        super();
    }

    public IdempotentException(String message) {
        super(message);
    }

    public IdempotentException(String message, Throwable cause) {
        super(message, cause);
    }

    public IdempotentException(Throwable cause) {
        super(cause);
    }

    protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
                                  boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

}

spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.basal.common.idempotent.config.IdempotentConfig