当前位置: 首页 > news >正文

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

文章目录

  • 1.简介
  • 2.后端防表单重复提交设计实现
    • 2.1.引入依赖
    • 2.2.添加redis配置
    • 2.3.添加需要使用的工具类
    • 2.4.添加防重复提交注解
    • 2.5.使用Aop实现限流逻辑
  • 3.测试
    • 3.1.添加需要限流的接口
    • 3.2.模拟表单重复提交操作

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;
        }
   });
 }

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

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>

2.2.添加redis配置

application.yml文件配置如下

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        max-active: 8

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

}

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);
    }

}

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);
    }
}

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;

}

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

}

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";
    }
}

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() + "个线程请求成功!");

    }

}

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

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

相关文章:

  • h5技术建设网站/成人电脑培训班办公软件
  • 哈尔滨座做网站的/建立网站用什么软件
  • 网站开发需要怎么做/网站快速优化排名软件
  • 德州市住房建设局网站/百度指数查询官方网
  • wordpress wp-postviews插件/网站推广的策略
  • 做动态网站可以不用框架吗/爱站网关键词查询网站的工具
  • Javaweb会话跟踪技术(Cookie和Session)
  • C++类和对象(三)
  • 第28章 MySQL 复制表教程
  • Java编程实战20:设计一个文本编辑器
  • 嵌入式微功耗RTU的功能与特点介绍、技术参数详情
  • ubuntu 安装supervisord
  • C++11标准模板(STL)- 算法(std::prev_permutation)
  • 【Vue】利用v-model特性封装Dialog弹窗或可编辑窗口。
  • 微信公众号迁移,需要做些什么
  • 白话说Java虚拟机原理系列【第三章】:类加载器详解
  • C++——STL之list详解
  • 【Numpy基础知识】结构化数组