当前位置:   article > 正文

【SpringBoot框架篇】32.基于注解+redis实现表单防重复提交_springboot 防止重复提交使用redis

springboot 防止重复提交使用redis

1.简介

在一些表单提交操作的时候会存在用户多次点击button触发提交事件的场景(针对异步请求场景)。

在客户端可以针对重复提交添加状态值判断,如下:

  • 1.声明一个loading变量,当触发submit事件的时候判断loading的值是否为true,为true则不进行操作
  • 2.如果判断的loading值为false,则发送请求提交数据到后台保存。
  • 3.当ajax异步处理成功的时候,把loading改为false(类似于释放锁);
var loading=false;
function submit(){
  if(loading){
	 return;
  }
  loading=true;
  $ajax.post("/api/user",user,function((res) => {
      loading=false;
        }
   });
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

众所周知,数据验证和表单防重复提交等逻辑在前端可以做判断,但是后台还是会再请求做一次校验。

2.后端防表单重复提交设计实现

2.1.引入依赖

pom.xml文件内容如下

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--redisson 分布式防表单提交用到的,单节点部署可以使用ecache基于内存的缓存-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.17.6</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>
    </dependencies>
  • 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

2.2.添加redis配置

application.yml文件配置如下

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        max-active: 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3.添加需要使用的工具类

MD5Util 工具类拥有对请求数据加密成md5(节省存储空间)

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MD5Util {

	public static String toMD5(String plainText) {
		String value = "";
		if (plainText == null){
			plainText = "";
		}
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update(plainText.getBytes());
			byte b[] = md.digest();

			int i;

			StringBuffer buf = new StringBuffer("");
			for (int offset = 0; offset < b.length; offset++) {
				i = b[offset];
				if (i < 0){
					i += 256;
				}
				if (i < 16){
					buf.append("0");
				}
				buf.append(Integer.toHexString(i));
			}
			value = buf.toString();

			// 24));// 16位的加密
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();

		}
		return value;
	}

}
  • 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

SpringUtil(普通类调用Spring bean对象使用的工具类)

@Component
public class SpringUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtil.applicationContext == null) {
            SpringUtil.applicationContext = applicationContext;
        }
    }

    /**
     * 获取applicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通过name获取 Bean.
     */
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    /**
     * 通过class获取Bean.
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     *通过name,以及Clazz返回指定的Bean
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}
  • 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

RedisUtils 操作redis缓存工具类

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RedisUtils {

    public static NameMapper getNameMapper() {
        Config config = getClient().getConfig();
        if (config.isClusterConfig()) {
            return config.useClusterServers().getNameMapper();
        }
        return config.useSingleServer().getNameMapper();
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key      缓存的键值
     * @param value    缓存的值
     * @param duration 时间
     */
    public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
        RBatch batch = getClient().createBatch();
        RBucketAsync<T> bucket = batch.getBucket(key);
        bucket.setAsync(value);
        bucket.expireAsync(duration);
        batch.execute();
    }
    
 	/**
     * 删除单个对象
     * @param key 缓存的键值
     */
    public static boolean deleteObject(final String key) {
        return getClient().getBucket(key).delete();
    }

    /**
     * 检查redis中是否存在key
     * @param key 缓存的键值
     */
    public static Boolean hasKey(String key) {
        RKeys rKeys = getClient().getKeys();
        return rKeys.countExists(getNameMapper().map(key)) > 0;
    }

    public static RedissonClient getClient() {
        return Lazy.CLIENT;
    }

    /**
     * 使用懒加载方式实例化RedissongetClient()客户端工具
     */
    private static class Lazy {
        private static final RedissonClient CLIENT = SpringUtil.getBean(RedissonClient.class);
    }
}

  • 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

2.4.添加防重复提交注解

/**
 * @author Dominick Li
 * @description 防止重复提交
 **/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 时间单位,默认为秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 间隔时间,默认为3秒
     */
    int interval() default 3;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2.5.使用Aop实现限流逻辑

使用@Around环绕通知实现逻辑

  • 1.根据限流注解配置的限流的时间获取缓存存活时间
  • 2.把请求的接口地址路径+用户token+请求参数作为缓存的Key
  • 3.判断缓存是否存在,如果存在则返回错误提示信息
  • 4.如缓存不存在则设置当前key的缓存数据,然后执行业务逻辑代码
/**
 * 防止重复提交AOP切面实现类
 */
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {

    @Pointcut("@annotation(com.ljm.boot.redisson.annotation.RepeatSubmit)")
    public void repeatSubmitPointCut() {
    }

    @Around("repeatSubmitPointCut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Method method = currentMethod(proceedingJoinPoint);
        //获取到方法的注解对象
        RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);

        long interval = 1000;
        if (repeatSubmit.interval() > 0) {
            interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
        }

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String params = argsToString(proceedingJoinPoint.getArgs());

        // 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 用户的唯一标识
        String token = request.getHeader("token");
        // 唯一标识(url +  token  + params)
        String submitKey = MD5Util.toMD5(url + "_" + token + ":" + params);

        boolean flag = false;

        //判断缓存中是否有此key
        if (RedisUtils.hasKey(submitKey)) {
            log.info("key={},interval={},重复提交", submitKey, interval);
        } else {
            //如果没有表示不是重复提交并设置key存活的缓存时间
            RedisUtils.setCacheObject(submitKey, "", Duration.ofMillis(interval));
            flag = true;
            System.out.println("非重复提交");
        }

        if (flag) {
            Object result = null;
            try {
                result = proceedingJoinPoint.proceed();
            } catch (Throwable e) {
                /*异常通知方法*/
                log.error("异常通知方法>目标方法名{},异常为:{}", method.getName(), e);
            } finally {
                RedisUtils.deleteObject(submitKey);
            }
            return result;
        } else {
            return "{'code':500,'msg':'重复提交'}";
        }
    }

    /**
     * 根据切入点获取执行的方法
     */
    private Method currentMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        //获取目标类的所有方法,找到当前要执行的方法
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method resultMethod = null;
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                resultMethod = method;
                break;
            }
        }
        return resultMethod;
    }

    /**
     * 参数拼装
     */
    private String argsToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (!ObjectUtils.isEmpty(o) && !isFilterObject(o)) {
                    try {
                        params.append(JSONObject.toJSONString(o)).append(" ");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return params.toString().trim();
    }

    /**
     * 判断是否是需要过滤的对象
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }

}
  • 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
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124

3.测试

3.1.添加需要限流的接口

@RestController
public class TestController {

    /**
     * 测试 间隔时间2秒
     */
    @GetMapping("/test/{id}")
    @RepeatSubmit(interval = 2)
    public String test(@PathVariable Integer id) throws Exception {
        Thread.sleep(1000L);
        return "success";
    }

    /**
     * 测试 间隔时间1500毫秒
     */
    @GetMapping("/test")
    @RepeatSubmit(interval = 1500, timeUnit = TimeUnit.MILLISECONDS)
    public String test2(@PathVariable Integer id) throws Exception {
        Thread.sleep(1000L);
        return "success";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

3.2.模拟表单重复提交操作

public class TestRepeatSubmit {

    public static void main(String[] args) throws Exception {
        ///设置线程池最大执行20个线程并发执行任务
        int threadSize = 20;
        //AtomicInteger通过CAS操作能保证统计数量的原子性
        AtomicInteger successCount = new AtomicInteger(0);
        CountDownLatch downLatch = new CountDownLatch(20);
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadSize);
        for (int i = 0; i < threadSize; i++) {
            int finalI = i;
            fixedThreadPool.submit(() -> {
                RestTemplate restTemplate = new RestTemplate();
                //String str = restTemplate.getForObject("http://localhost:8032/test/"+i, String.class);
                String str = restTemplate.getForObject("http://localhost:8032/test/1", String.class);
                if ("success".equals(str)) {
                    successCount.incrementAndGet();
                }
                System.out.println(str);
                downLatch.countDown();
            });
            //模拟网络传输时间
            Thread.sleep(100);
        }
        //等待所有线程都执行完任务
        downLatch.await();
        fixedThreadPool.shutdown();
        System.out.println("总共有" + successCount.get() + "个线程请求成功!");

    }

}

  • 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

启动web服务后,
1.访问地址使用http://localhost:8032/test/1,运行main函数结果如下,注解配置的间隔时间内相同参数的请求会被拒绝。
在这里插入图片描述
2.访问地址使用http://localhost:8032/test/i,运行main函数结果如下:

在这里插入图片描述
由于i是不同的值,所以20个请求都能够正常请求。

4.项目配套代码

gitee代码地址

创作不易,要是觉得我写的对你有点帮助的话,麻烦在gitee上帮我点下 Star

【SpringBoot框架篇】其它文章如下,后续会继续更新。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/425933?site
推荐阅读
相关标签
  

闽ICP备14008679号