精华内容
下载资源
问答
  • 我们大家再进行web开发的时候,必不可少会遇见表单重复提交问题。来给总结如何解决表单提交问题,欢迎大家交流指正。  首先我们在讨论如何解决表单重复提交问题之前先来解决三个问题:1.什么叫表单重复提交?2.什么...
  • 表单请求提交到本

    千次阅读 2019-02-24 12:50:31
    Web应用程序中,有时需要将表单请求提交到本进行处理 实现将表单提交到本,只要将表单的action属性设置为本即可,假定表单页为index.jsp ,那么action的值为index.jsp,然后应用request对象的getParameter()...

    在Web应用程序中,有时需要将表单请求提交到本页进行处理

    实现将表单提交到本页,只要将表单的action属性设置为本页即可,假定表单页为index.jsp
    ,那么action的值为index.jsp,然后应用request对象的getParameter()方法来获取表单元素的值。
    在这里插入图片描述

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    <form action="index8.jsp" method="post">
    	<table align="center" height="150" height="300">
    		<tr><td align="center" colspan="4" height="20"></td></tr>
    		<tr><td align="center" colspan="4">用户登录</td></tr>
    		<tr>
    			<td>用户名:</td>
    			<td><input type="text" name="name"></td>
    		</tr>
    		<tr>
    			<td>密码:</td>
    			<td><input type="password" name="pwd"></td>
    		</tr>
    		<tr>
    			<td></td>
    			<td><input type="submit" name="submit" value="登录"></td>
    		</tr>
    		<%
    		if(request.getParameter("submit")!=null){
    		request.setCharacterEncoding("UTF-8");
    		String name=request.getParameter("name");
    		String pwd=request.getParameter("pwd");
    		
    		%>
    		<tr>
    			<td>用户名参数为:</td>
    			<td><%=name %></td>
    		</tr>
    		<tr>
    			<td>密码参数为:</td>
    			<td><%=pwd %></td>
    		</tr>
    		<%} %>
    	</table>
    </form>
    </body>
    </html>
    

    在index.jsp页中获取表单信息,关键代码:

    <%
    		if(request.getParameter("submit")!=null){
    		request.setCharacterEncoding("UTF-8");
    		String name=request.getParameter("name");
    		String pwd=request.getParameter("pwd");
    		
    		%>
    

    需要注意的

    ,在页面中使用request对象的getParameter()方法来获取参数时,在页面加载后就会自动执行这段代码,因此,需要 if(request.getParameter("submit")!=null)进行合理判断,只有提交表单后才可以执行request对象的getParameter()方法来获取表单的代码

    展开全文
  • SpringBoot/Web项目防止表单/请求重复提交(单机和分布式) 一、场景/方案 说起web项目的防止表单/请求重复提交,不得不说幂等性。 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。...

    SpringBoot/Web项目防止表单/请求重复提交(单体和分布式)

    一、场景/方案

    说起web项目的防止表单/请求重复提交,不得不说幂等性。

    幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。

    1.1、常见场景:

    	•	订单接口, 不能多次创建订单
    	•	支付接口, 重复支付同一笔订单只能扣一次钱
    	•	支付宝回调接口, 可能会多次回调, 必须处理重复回调
    	•	普通表单提交接口, 因为网络超时,卡顿等原因多次点击提交, 只能成功一次等等
    

    1.2、常见方案

    解决思路:

    1. 从数据库方面考虑,数据设计的时候,如果有唯一性,考虑建立唯一索引。
    2. 从应用层面考虑,首先判断是单机服务还是分布式服务?
      • 单机服务:考虑一些缓存Cacha,利用缓存,来保证数据的重复提交。
      • 分布式服务,考虑将用户的信息,例如token和请求的url进行组装在一起形成令牌,存储到缓存中,例如redis,并设置超时时间为xx秒,如此来保证数据的唯一性。(利用了redis的分布式锁)

    解决方案大致总结为:

    	- 唯一索引 -- 防止新增脏数据
    	- token机制 -- 防止页面重复提交,实现接口幂等性校验
    	- 分布式锁 -- redis(jedis、redisson)或zookeeper实现
    	- 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
    	- 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
    	- 状态机 -- 状态变更, 更新数据时判断状态
    

    前三种方式最为常见,本文则从应用层面考虑,给出单机服务还是分布式服务下的解决方案。

    二、单体服务项目-防止重复提交

    比如你的项目是一个单独springboot项目,SSM项目,或者其他的单体服务,就是打个jar或者war直接扔服务器上跑的。

    采用【AOP解析自定义注解+google的Cache缓存机制】来解决表单/请求重复的提交问题。

    思路:

    1. 建立自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
    2. 通过AOP机制对所有标记了@NoRepeatSubmit 的方法拦截。
    3. 在业务方法执行前,使用google的缓存Cache技术,来保证数据的重复提交。
    4. 业务方法执行后,释放缓存。

    好了,接下里就是新建一个springboot项目,然后开整了。
    在这里插入图片描述

    2.1 pom.xml新增依赖

    需要新增一个google.common.cache.Cache;

    源码如下:

      <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>24.0-jre</version>
      </dependency>
    

    2.2 新建NoRepeatSubmit.java自定义注解类

    一个自定义注解接口类,是interface类哟 ,里面什么都不写,为了就是个重构。
    源码如下:

    package com.gitee.taven.aop;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @title: NoRepeatSubmit
     * @Description:  自定义注解,用于标记Controller中的提交请求
     * @Author: ZouTao
     * @Date: 2020/4/14
     */
    @Target(ElementType.METHOD)  // 作用到方法上
    @Retention(RetentionPolicy.RUNTIME) // 运行时有效
    public @interface NoRepeatSubmit {
    
    }
    

    2.3 新建NoRepeatSubmitAop.java

    这是个AOP的解析注解类,使用到了Cache缓存机制。
    以cache.getIfPresent(key)的url值来进行if判断,如果不为空,证明已经发过请求,那么在规定时间内的再次请求,视为无效,为重复请求。如果为空,则正常响应请求。
    源码如下:

    package com.gitee.taven.aop;
    
    import javax.servlet.http.HttpServletRequest;
    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.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import com.google.common.cache.Cache;
    
    /**
     * @Description: aop解析注解-配合google的Cache缓存机制
     * @Author: Zoutao
     * @Date: 2020/4/14
     */
    @Aspect
    @Component
    public class NoRepeatSubmitAop {
    
        private Log logger = LogFactory.getLog(getClass());
    
        @Autowired
        private Cache<String, Integer> cache;
    
        @Pointcut("@annotation(noRepeatSubmit)")
        public void pointCut(NoRepeatSubmit noRepeatSubmit) {
        }
        
        @Around("pointCut(noRepeatSubmit)")
        public Object arround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) {
            try {
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                String sessionId = RequestContextHolder.getRequestAttributes().getSessionId();
                HttpServletRequest request = attributes.getRequest();
                String key = sessionId + "-" + request.getServletPath();
                if (cache.getIfPresent(key) == null) {// 如果缓存中有这个url视为重复提交
                    Object o = pjp.proceed();
                    cache.put(key, 0);
                    return o;
                } else {
                    logger.error("重复请求,请稍后在试试。");
                    return null;
                }
            } catch (Throwable e) {
                e.printStackTrace();
                logger.error("验证重复提交时出现未知异常!");
                return "{\"code\":-889,\"message\":\"验证重复提交时出现未知异常!\"}";
            }
        }
    }
    

    2.4 新建缓存类UrlCache.java

    用来获取缓存和设置有效期,目前设置有效期为2秒。
    源码如下:

    package com.gitee.taven;
    
    import java.util.concurrent.TimeUnit;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import com.google.common.cache.Cache;
    import com.google.common.cache.CacheBuilder;
    
    /**
     * @Description: 内存缓存配置类
     * @Author: Zoutao
     * @Date: 2020/4/14
     */
    
    @Configuration
    public class UrlCache {
        @Bean
        public Cache<String, Integer> getCache() {
            return CacheBuilder.newBuilder().expireAfterWrite(2L, TimeUnit.SECONDS).build();// 缓存有效期为2秒
        }
    }
    

    2.5 新建CacheTestController.java

    一个请求控制类,用来模拟响应请求和业务处理。

    源码如下:

    package com.gitee.taven.controller;
    
    import com.gitee.taven.ApiResult;
    import com.gitee.taven.aop.NoRepeatSubmit;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @Description: 测试Cache方式的Controller
     * @Author: Zoutao
     * @Date: 2020/4/14
     */
    @RestController
    public class CacheTestController {
    
        private Object data;
    
        @RequestMapping("/TestSubmit")
        @NoRepeatSubmit()
        public Object test() {
            data = "程序逻辑返回,假设是一大堆DB来的数据。。。";
            return new ApiResult(200, "请求成功",data);
            // 也可以直接返回。return (",请求成功,程序逻辑返回");
        }
    }
    

    ps:这里可以在建立一个ApiResult.java类,来规范返回的数据格式体:
    ApiResult.java(非必须)
    源码如下:

    package com.gitee.taven;
    
    /** 
     * @title: ApiResult
     * @Description:  统一规范结果格式
     * @Param:   code, message, data
     * @return:  ApiResult
     * @Author: ZouTao 
     * @Date: 2020/4/14 
     */
    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 +
                    '}';
        }
    }
    
    

    纯粹为了规范而加的,你可以不用。

    2.6 启动项目

    运行springboot项目,启动成功后,在浏览器输入:http://localhost:8080/TestSubmit
    然后F5刷新(模拟重复发起请求),查看效果:

    在这里插入图片描述
    可以看到只有一次请求被成功响应,返回了数据,在有效时间内,其他请求被判定为重复提交,不予执行。

    目前防止重复提交最简单的方案(最新)

    两个关键信息,第一:防止重复提交;第二:最简单。
    前提是是单机环境下。

    在这里插入图片描述

    1.前端拦截

    前端拦截是指通过 HTML 页面来拦截重复请求,比如在用户点击完“提交”按钮后,我们可以把按钮设置为不可用或者隐藏状态。

    实现代码:

    <html>
    <script>
        function subAdd(){
            // 按钮设置为不可用
            document.getElementById("btn_sub").disabled="disabled";
            document.getElementById("dv1").innerText = "按钮被点击了~";
        }
    </script>
    <body style="margin-top: 100px;margin-left: 100px;">
        <input id="btn_sub" type="button"  value="添 加"  onclick="subAdd()">
        <div id="dv1" style="margin-top: 80px;"></div>
    </body>
    </html>
    

    执行效果:
    前端拦截重复请求

    但前端拦截有一个致命的问题,如果是懂行的程序员或非法用户可以直接绕过前端页面,所以除了前端拦截一部分正常的误操作之外,后端的拦截也是必不可少。

    2.后端拦截-DCL方案

    后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。

    提及将数据存储在内存中,最简单的方法就是使用 HashMap 存储,HashMap 的防重(防止重复)版本也是最原始的 。缺点是 HashMap 是无限增长的,因此它会占用越来越多的内存,并且随着 HashMap 数量的增加查找的速度也会降低,已不再推荐。

    推荐使用最新的单例中著名的 DCL(Double Checked Locking,双重检测锁)来防止重复提交。

    原理不需要深究,好在 Apache 为我们提供了一个 commons-collections 的框架,里面有一个非常好用的数据结构 LRUMap 可以保存指定数量的固定的数据,并且它就会按照 LRU 的算法,帮你清除最不常用的数据。

    LRU 是 Least Recently Used 的缩写,即最近最少使用,是一种常用的数据淘汰算法,选择最近最久未使用的数据予以淘汰。

    L R U M a p 版 防 止 重 复 提 交 方 案 : \color{red}{LRUMap版防止重复提交方案:} LRUMap

    1.首先,我们先来添加 Apache commons collections 的引用:

    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-collections4</artifactId>
      <version>4.4</version>
    </dependency>
    

    本文已封装为一个公共的方法,以供所有类使用。

    实现代码如下:

    import org.apache.commons.collections4.map.LRUMap;
    
    /**
     * 幂等性判断
     * 使用LRUMap。
     */
    public class IdempotentUtils {
    
        // 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个
        private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);
    
        /**
         * 幂等性判断
         * @return
         */
        public static boolean judge(String id, Object lockClass) {
            synchronized (lockClass) {
                // 重复请求判断
                if (reqCache.containsKey(id)) {
                    // 重复请求
                    System.out.println("请勿重复提交!!!" + id);
                    return false;
                }
                // 非重复请求,存储请求 ID
                reqCache.put(id, 1);
            }
            return true;
        }
    }
    

    调用代码:

    import com.example.idempote.util.IdempotentUtils;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RequestMapping("/user")
    @RestController
    public class UserController4 {
        @RequestMapping("/add")
        public String addUser(String id) {
            // 非空判断(忽略)...
            // -------------- 幂等性调用(开始) --------------
            if (!IdempotentUtils.judge(id, this.getClass())) {
                return "执行失败";
            }
            // -------------- 幂等性调用(结束) --------------
            // 业务代码...
            System.out.println("添加用户ID:" + id);
            return "执行成功!";
        }
    }
    

    当然,熟悉spring注解的朋友,可以通过自定义注解,将业务代码写到注解中,需要调用的方法只需要写一行注解就可以防止数据重复提交了。

    注意:
    1.DCL 适用于重复提交频繁比较高的业务场景,对于相反的业务场景下 DCL 并不适用。
    2.LRUMap 的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表 header 的前一个位置,在新增元素时,如果容量满了就会移除 header 的后一个元素。

    效果:
    在这里插入图片描述

    上诉方式仅适用于单机环境下的重复数据拦截,如果是分布式环境需要配合数据库或 Redis 来实现。

    三、分布式服务项目-防止重复提交

    如果你的spirngboot项目,后面要放到分布式集群中去使用,那么这个单体的Cache机制怕是会出问题,所以,为了解决项目在集群部署时请求可能会落到多台机器上的问题,我们把内存缓存换成了redis

    利用token机制+redis的分布式锁(jedis)来防止表单/请求重复提交。

    思路如下:

    1. 自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
    2. 通过AOP 对所有标记了 @NoRepeatSubmit 的方法拦截。
    3. 在业务方法执行前,获取当前用户的 token(或JSessionId)+ 当前请求地址,形成一个唯一Key,然后去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。
    4. 最后业务方法执行完毕,释放锁。

    3.1 Application配置redis

    打开application.properties或application.yml配置redis:
    内容如下:

    server.port=8080
    
    # Redis数据库索引(默认为0)
    spring.redis.database=0  
    # Redis服务器地址
    spring.redis.host=localhost
    # Redis服务器连接端口
    spring.redis.port=6379  
    # Redis服务器连接密码(默认为空)
    #spring.redis.password=yourpwd
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.jedis.pool.max-active=8  
    # 连接池最大阻塞等待时间
    spring.redis.jedis.pool.max-wait=-1ms
    # 连接池中的最大空闲连接
    spring.redis.jedis.pool.max-idle=8  
    # 连接池中的最小空闲连接
    spring.redis.jedis.pool.min-idle=0  
    # 连接超时时间(毫秒)
    spring.redis.timeout=5000ms
    

    3.2 pom.xml新增依赖

    pom.xml需要一些redis的依赖,使用Redis 是为了在负载均衡部署,

    直接贴出整个项目的吧:

    <?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">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.1.3.RELEASE</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.gitee.taven</groupId>
    	<artifactId>repeat-submit-intercept</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>repeat-submit-intercept</name>
    	<description>Demo project for Spring Boot</description>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    		<java.version>1.8</java.version>
    	</properties>
    	<dependencies>
    		<!--方式一:缓存类-->
    		<dependency>
    			<groupId>com.google.guava</groupId>
    			<artifactId>guava</artifactId>
    			<version>24.0-jre</version>
    		</dependency>
    		<!--方式二:redis类-->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-redis</artifactId>
    			<exclusions>
    				<exclusion>
    					<groupId>redis.clients</groupId>
    					<artifactId>jedis</artifactId>
    				</exclusion>
    				<exclusion>
    					<groupId>io.lettuce</groupId>
    					<artifactId>lettuce-core</artifactId>
    				</exclusion>
    			</exclusions>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-aop</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-devtools</artifactId>
    			<scope>runtime</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>redis.clients</groupId>
    			<artifactId>jedis</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.commons</groupId>
    			<artifactId>commons-pool2</artifactId>
    		</dependency>
    	</dependencies>
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    </project>
    

    3.3 自定义注解NoRepeatSubmit.java

    也是一个自定义注解,其中设置请求锁定时间。

    package com.gitee.taven.aop;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @title: NoRepeatSubmit
     * @Description:  自定义注解,用于标记Controller中的提交请求
     * @Author: ZouTao
     * @Date: 2020/4/14
     */
    @Target(ElementType.METHOD)  // 作用到方法上
    @Retention(RetentionPolicy.RUNTIME) // 运行时有效
    public @interface NoRepeatSubmit {
        /*
         * 防止重复提交标记注解
         * 设置请求锁定时间
         * @return
         */
        int lockTime() default 10;
    }
    

    3.4 AOP类RepeatSubmitAspect:

    一个AOP解析注解类。
    获取当前用户的Token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,然后以Key去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。)

    源码如下:

    package com.gitee.taven.aop;
    
    import com.gitee.taven.ApiResult;
    import com.gitee.taven.utils.RedisLock;
    import com.gitee.taven.utils.RequestUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.util.Assert;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.UUID;
    
    /**
     * @title: RepeatSubmitAspect
     * @Description: AOP类解析注解-配合redis-解决程序集群部署时请求可能会落到多台机器上的问题。
     * 作用:对标记了@NoRepeatSubmit的方法进行拦截
     * @Author: ZouTao
     * @Date: 2020/4/14
     */
    @Aspect
    @Component
    public class RepeatSubmitAspect {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class);
    
        @Autowired
        private RedisLock redisLock;
    
        @Pointcut("@annotation(noRepeatSubmit)")
        public void pointCut(NoRepeatSubmit noRepeatSubmit) {
        }
    
        /**
         * @title: RepeatSubmitAspect
         * @Description:在业务方法执行前,获取当前用户的
         * token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,
         * 去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁。)
         * @Author: ZouTao
         * @Date: 2020/4/14
         */
    
        @Around("pointCut(noRepeatSubmit)")
        public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
            int lockSeconds = noRepeatSubmit.lockTime();
    
            HttpServletRequest request = RequestUtils.getRequest();
            Assert.notNull(request, "request can not null");
    
            // 此处可以用token或者JSessionId
            String token = request.getHeader("Authorization");
            String path = request.getServletPath();
            String key = getKey(token, path);
            String clientId = getClientId();
    
            boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
            LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId);
            // 主要逻辑
            if (isSuccess) {
                LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
                // 获取锁成功
                Object result;
                try {
                    // 执行进程
                    result = pjp.proceed();
                } finally {
                    // 解锁
                    redisLock.releaseLock(key, clientId);
                    LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
                }
                return result;
            } else {
                // 获取锁失败,认为是重复提交的请求。
                LOGGER.info("tryLock fail, key = [{}]", key);
                return new ApiResult(200, "重复请求,请稍后再试", null);
            }
        }
    
        // token(或者JSessionId)+ 当前请求地址,作为一个唯一KEY
        private String getKey(String token, String path) {
            return token + path;
        }
    
        // 生成uuid
        private String getClientId() {
            return UUID.randomUUID().toString();
        }
    }
    

    3.5 请求控制类SubmitController

    这是一个测试接口的请求控制类,模拟业务场景,

    package com.gitee.taven.controller;
    
    import com.gitee.taven.ApiResult;
    import com.gitee.taven.aop.NoRepeatSubmit;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @title: SubmitController
     * @Description:  测试接口
     * @Author: ZouTao
     * @Date: 2020/4/14
     */
    @RestController
    public class SubmitController {
    
        @PostMapping("submit")
        @NoRepeatSubmit()
        public Object submit(@RequestBody UserBean userBean) {
            try {
                // 模拟业务场景
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return new ApiResult(200, "成功", userBean.userId);
        }
    
        public static class UserBean {
            private String userId;
    
            public String getUserId() {
                return userId;
            }
    
            public void setUserId(String userId) {
                this.userId = userId == null ? null : userId.trim();
            }
        }
    }
    

    3.6 Redis分布式锁实现

    需要一个工具类来实现Redis分布式锁,具体实现原理请参考另外一篇文章。这里贴出源码。

    新建RedisLock类,如下:

    package com.gitee.taven.utils;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Service;
    import redis.clients.jedis.Jedis;
    
    import java.util.Collections;
    
    /** 
     * @title: RedisLock
     * @Description:  Redis 分布式锁实现
     * @Author: ZouTao 
     * @Date: 2020/4/14 
     */
    @Service
    public class RedisLock {
    
        private static final Long RELEASE_SUCCESS = 1L;
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
        private static final String SET_WITH_EXPIRE_TIME = "EX";
        // if get(key) == value return del(key)
        private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        /**
         * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
         * 对于 Redis 集群则无法使用
         *
         * 支持重复,线程安全
         *
         * @param lockKey   加锁键
         * @param clientId  加锁客户端唯一标识(采用UUID)
         * @param seconds   锁过期时间
         * @return
         */
        public boolean tryLock(String lockKey, String clientId, long seconds) {
            return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
                Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
                if (LOCK_SUCCESS.equals(result)) {
                    return true;
                }
                return false;
            });
        }
    
        /**
         * 与 tryLock 相对应,用作释放锁
         *
         * @param lockKey
         * @param clientId
         * @return
         */
        public boolean releaseLock(String lockKey, String clientId) {
            return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
                Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
                        Collections.singletonList(clientId));
                if (RELEASE_SUCCESS.equals(result)) {
                    return true;
                }
                return false;
            });
        }
    }
    

    顺便新建一个RequestUtils工具类,用来获取一下getRequest的。

    RequestUtils.java 如下:

    package com.gitee.taven.utils;
    
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    
    /** 
     * @title: RequestUtils
     * @Description:  获取 Request 信息
     * @Author: ZouTao 
     * @Date: 2020/4/14 
     */
    public class RequestUtils {
    
        public static HttpServletRequest getRequest() {
            ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return ra.getRequest();
        }
    
    }
    
    

    3.7 自动测试类RunTest

    在上一个示例代码中,我们采用了启动项目,访问浏览器,手动测试的方式,接下里这个,
    参考以前的一篇文章springboot启动项目自动运行测试方法,使用自动测试类来模拟测试

    模拟了10个并发请求同时提交:

    package com.gitee.taven.test;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.http.HttpEntity;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Component;
    import org.springframework.web.client.RestTemplate;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /** 
     * @title: RunTest
     * @Description:  多线程测试类
     * @Param:  模拟十个请求并发同时提交
     * @return:  
     * @Author: ZouTao 
     * @Date: 2020/4/14 
     */
    @Component
    public class RunTest implements ApplicationRunner {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(RunTest.class);
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("=================执行多线程测试==================");
            String url="http://localhost:8000/submit";
            CountDownLatch countDownLatch = new CountDownLatch(1);
            ExecutorService executorService = Executors.newFixedThreadPool(10);  //线程数
    
            for(int i=0; i<10; i++){
                String userId = "userId" + i;
                HttpEntity request = buildRequest(userId);
                executorService.submit(() -> {
                    try {
                        countDownLatch.await();
                        System.out.println("Thread:"+Thread.currentThread().getName()+", time:"+System.currentTimeMillis());
                        ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
                        System.out.println("Thread:"+Thread.currentThread().getName() + "," + response.getBody());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
            countDownLatch.countDown();
        }
    
        private HttpEntity buildRequest(String userId) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Authorization", "yourToken");
            Map<String, Object> body = new HashMap<>();
            body.put("userId", userId);
            return new HttpEntity<>(body, headers);
        }
    }
    

    3.8 启动项目

    启动项目,先启动redis,再运行springboot,会自动执行测试方法,然后控制台查看结果。

    在这里插入图片描述

    成功防止重复提交,控制台日志,可以看到十个线程的启动时间几乎同时发起,只有一个请求提交成功了。
    在这里插入图片描述

    ps:
    有些人使用jedis3.1.0版本貌似已经没有这个set方法,则可以改为:

    String result = jedis.set(lockKey, clientId, new SetParams().nx().px(seconds)); 
    

    也ok了

    整体项目结构图:
    在这里插入图片描述
    两套解决方案都在里面了,其中NoRepeatSubmit自定义注解类是共用的,区别在于有一个int lockTime()方法,不是使用redis的时候,注释掉即可。


    上述就是SpringBoot/Web项目中防止表单/请求重复提交的一个方案,分为单机和分布式环境下。有什么疑问请留言吧。需要源码可以评论留下邮箱,后期会贴出git地址。

    参考地址:
    [1]: https://www.jianshu.com/p/09c6b05b670a

    展开全文
  • 例如点击在线询盘,点击后弹出窗口,只有在提交信息成功后,先关闭弹出的层,然后再回来刷新这个列表。 //iframe层 layer.open({ type: 2, title: '弹出窗口', shadeClose: true, shade: 0.8, area: ['...

     例如点击在线询盘,点击后弹出窗口,只有在提交信息成功后,先关闭弹出的层,然后再回来刷新这个列表页。

     

    //iframe层
    layer.open({
      type: 2,
      title: '弹出窗口',
      shadeClose: true,
      shade: 0.8,
      area: ['680px', '600px'],
      content: '弹出窗口的url地址', //iframe的url
      end: function(){
    	//window.location.reload();//写在这里的话不管是否提交成功都会刷新这个页面 
    	return false;
      }
    }); 
    $.ajax({
    	type:"POST",
    	dataType:"json",
    	url:"/web/ajax.php",
    	data:{
    		gongneng:"liuyan",
    		classid:classid,
    		title:title,
    		username:username,
    	},
    	success:function(result){
    		layer.msg(result.msg,{icon: 6,area:['450px', ''],shade : [0.5 , '#000' , true]});
    		var index = parent.layer.getFrameIndex(window.name);
    		parent.layer.close(index);//关闭弹出的子页面窗口
    		window.parent.location.reload();//写在这里的话是只有提交成功了才刷新那个页面 
    	},
    	error:function(){
    		layer.msg(result.msg,{icon: 2,area:['450px', ''],shade : [0.5 , '#000' , true]});
    	}
    })

     

    展开全文
  • 表单提交的问题一直困扰着我这个新手,现在也...用到了 jQuery Form Plugin,表单提交的第三方插件,为了更方便的管理和提交表单。(官网:http://plugins.jquery.com/form/) 借鉴:https://blog.csdn.net/m0_3750...

    表单提交的问题一直困扰着我这个新手,现在也算学习了不少,解决了我现在有的问题 —— 提交了表单之后将数据给到后台并且让前端页面跳转到想要的页面或出现想要的内容。

    用到了 jQuery Form Plugin,表单提交的第三方插件,为了更方便的管理和提交表单。(官网:http://plugins.jquery.com/form/

    借鉴:https://blog.csdn.net/m0_37505854/article/details/79639046(主要)

    https://www.cnblogs.com/hudandan/p/5912336.html

    我在这只写如何提交并显示想要的内容(跳转到页面同理),其中有好多概念什么的,我没列举出来,不了解的可以先看一遍官网的文档或其他的相关文档,有个大概的了解之后再去参考别人写的代码,就会好理解很多,我就是这样做的。

    1 引入 jQuery.js 和 jquery-form.js:

    <script src="sources/jquery-1.9.1.js"></script>
    <script src="sources/jquery-form.js"></script>

    2 HTML 里写的代码就是很普通的表单控件:

    <div id="showForm">
        <form action="/postform" method="post" id="ajaxForm">   <!--postform 是用于提交的地址-->
            <input type="text" name="user">
            <input type="number" name="age">
            <button type="submit">提交</button>
        </form>
    </div>

    3 js 代码:

    重点就是 js 代码的部分了,这是实现表单提交之后的显示的!

    <script>
        $(      //页面加载完执行
            $("#ajaxForm").on("submit",function () {    //表单提交时监听提交事件
                $(this).ajaxSubmit(options);    //当前表单执行异步提交,optons 是配置提交时、提交后的相关选项
                return false;   //  必须返回false,才能跳到想要的页面
            })
        )
        //配置 options 选项
        var options = {
            url: "/postform",       //提交地址:默认是form的action,如果申明,则会覆盖
            type: "post",           //默认是form的method(get or post),如果申明,则会覆盖
            success: successFun,    //提交成功后的回调函数,即成功后可以定页面跳到哪里
            dataType: "json",       //接受服务端返回的类型
            clearForm: true,        //成功提交后,清除所有表单元素的值
            resetForm: true,        //成功提交后,重置所有表单元素的值
            timeout: 3000           //设置请求时间,超过该时间后,自动退出请求,单位(毫秒)
        }
        //设置提交成功后返回的页面
        function successFun(data,status) {
            $("#showForm").html(data.msg);      //提交表单后从后台接收到的返回来的数据,我保存到了msg里
            // $("#showForm").html("或者这里可以直接写想要显示的内容")
        }
    </script>

    我用的是 nodejs (非必须):

    app.post("/postform",function (req, res) {
            console.log(req.body);
            userLink.create({
                "name": req.body.user,
                "age": req.body.age
            },function (err) {
                if(err) throw err;
                res.status(200).json({success: true,msg: "提交成功,这是你想要返回的页面。"});
            })
        })

    其实写 nodejs 是因为要给一个可以提交过去的地址,一般都是后台给的,为了记录就写上了。

    有不足之处欢迎指正,共同学习。

    (⌒▽⌒)

     

    展开全文
  • 通过action属性提交HTML表单

    千次阅读 2018-05-08 08:06:43
    总结一点关于表单提交的基础知识A....lt;form&gt;来定义,是用来收集用户输入的。... 2.method 规定在提交表单是使用的HTTP方法(get, post) GET 被提交的表单数据会显示在页面地址栏,安全性较...
  • layui提交成功之后,关闭当前页面

    千次阅读 2020-06-08 15:13:33
    layer.msg(result.msg, {icon: 1, time: 500}, function () { parent.layer.closeAll(); parent.reload_data(); }); result.msg:后台返回的消息 parent.layer.closeAll();//关闭所有弹窗 ......
  • jquery表单form提交阻止页面全部刷新

    千次阅读 2018-08-21 19:46:57
    所以出现的问题是:当用户往输入框内输入内容分后,按回车或者点击提交按钮,往当前页面输出内容的局部刷新会被之后的整个页面刷新给刷掉。 为了阻止整个页面刷新而保留局部刷新,我们可以使用以下方法: 方法一:...
  • 关键:serialize
  • 1、 我们做表单提交时如果提交成功则跳转到成功页面或其他业务逻辑页面,如果失败要在表单里提示用户哪些字段参数错了,由于form submit后会自动刷新页面,这样就无法在用户原来编辑的表单提醒用户哪些字段出了错误...
  • 表单重复提交解决方案(防止Http重复提交 场景模拟 创建一个from.jsp页面 &amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&...
  • 03- web表单测试

    千次阅读 2019-06-21 15:06:00
    表单测试 1.用户注册,登录,信息提交。 2.用户查询商品。 3.用户订购商品。 4.用户查询订单等。 表单测试实例 表单数据添加测试(一) 添加按钮可用,测试点击添加按钮,能够进入响应的添加页面...
  • html中提交表单并实现不跳转页面处理返回值

    万次阅读 多人点赞 2015-10-22 13:47:48
    本文中实现在html中使用jquery的组件实现表单提交并在当前页面中处理返回值。 1、使用AjaxForm方式 (1)、导入js文件  需要导入相关的js文件。使用ajaxForm方式需要使用到jquery和jquery-form两个js文件。如: ...
  • 方法一、 应为提交是上一次正常提交的... 在Web开发中,必须面对的问题就是表单的重复提交问题(这里仅指F5刷新造成的重复提交),.NET中处理这个问题似乎没有什么好的方法。 在网上搜索得到的解决方法主要有两种
  • web前端—列表和表单

    千次阅读 2016-12-22 21:43:08
    定义列表 咖啡 黑色热饮料 <dt></dt> <dd></dd> 无序列表 </ul><ol> <li><li> 表单表单提交地址" method="提交方法"> ...</form>action为空,向当前页面提交 get提交参数在地址栏可见,post不显示,
  • 1、action处理完用户提交的数据后,重定向到另一个action或者页面中,提交之后,所停留的位置不是当前处理数据的action了,再刷新也是无法插入数据。 struts.xml: &lt;action name="action1" class=...
  • 防止表单重复提交(只通过后台实现)

    万次阅读 热门讨论 2019-03-26 18:15:18
    黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。 这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。 使用AOP...
  • 1. HTML提交表单 HTML提交表单简单易操作,依靠在&lt;form&gt;标签对中的&lt;input type='submit'&gt;提交按钮进行请求发送和参数提交。其中form标签的post属性决定提交方式是get还是post。  jsp...
  • WEB表单设计学习心得

    千次阅读 2014-12-03 22:25:08
    在我们的系统中,有着各式表单,我们的日常工作也和表单分不开……WEB表单对于关键互动(比如注册和支付)起着至关重要的中介作用。据统计,经过重新设计的表单,完成率往往能提高10%~40%。这意味着完成新销售或者...
  • 使用浏览器后退按钮重复之前的操作,导致重复提交表单。使用浏览器历史记录重复提交表单。浏览器重复的HTTP请求。用户提交表单时可能因为网速的原因,或者网页被恶意刷新,致使同一条记录重复插入到数据库中,这是一...
  • 怎样防止表单重复提交,通过搜索引擎能搜到很多结果,但很零散,系统性不强,正好前几天做了这个功能,决定记录下来。 根据数据流向的过程,分别在三个“点”控制表单的重复提交,如下:第一,用户触发submit时,...
  • HTML表单提交后不弹出新窗口

    千次阅读 2018-12-04 19:48:38
    一、修改form的target属性 ()" target="_parent" > ... 登录" /> ... 可以选择在当前界面打开或者是新建一个界面打开,...四、提交表单后返回上一界面(刷新) location.replace(document.referrer)    
  • 今天终于理解这个地址的问题,对于jsp页面或者是servlet中的相对地址和绝对地址,这里我需要说的几点:首先对于相对地址来说:Jsp页面我现在有一个项目,名称是shengsiyuan,然后webroot下面有一个文件夹session,session...
  • a标签实现表单提交功能

    千次阅读 2019-03-07 16:35:13
    提交 a > form > JS < script > $ ( function ( ) { $ ( "#btnA" ) . click ( function ( ) { $ ( "#myForm" ) . submit ( ) ; } ) ; } ) ; < / script >
  • Django框架开发中避免表单重复提交

    千次阅读 2018-09-08 17:35:34
    Form表单做为web2.0时代的重要角色,也是我们与web网站进行数据交互的重要渠道,但是大家在web网站开发过程中,都会遇到一个问题,那就是如何避免表单重复提交,我们可不确定用户可在提交了一个表单后,是否有足够的...
  • 在做搜索分页时表单get提交时他自动跳至首页,会把跳转地址给清除(覆盖)掉 正常的地址应该是这样 http://localhost/space/frontend/web/index.php?r=space-write/teacher&shop_id=87&d...
  • 前后端分离之form表单提交

    千次阅读 2020-03-13 17:17:01
    原有的form表单会随着提交而跳转到另外一个页面,但是前后端分离之后,前端多是SPA页面,在不指定action时会提交当前页面,指定action会跳转到其它页面,而无论哪一种操作,都会引发当前页面的刷新,导致数据提交...
  • 本文主要是作者《中小型网站开发与设计》课程的内容,非常基础的文章,主要是指导学生学会用MyEclipse实现JSP网页表单提交及传递参数。希望大家喜欢这篇文章,基础文章,不喜勿喷~ 2.查看网页JSP源代码,位于...
  • 表单提交的几种方式

    万次阅读 2017-08-14 15:31:34
    一、Form和提交 ...method : 表单提交方式,默认为get,可选值为get/post enctype : 规定在向服务器发送表单数据之前如何对其进行编码。(适用于 method="post" 的情况), 可取值为application/x-www-form-
  • 1 在你的表单页里HEAD区加入这段代码: 2 生成一个令牌保存在用户session中,在form中加一个hidden域,显示该令 牌的值,form提交后重新生成一个新的令牌,将用户提交的令牌和session 中的令牌比较...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 102,326
精华内容 40,930
关键字:

web提交表单当前页