springboot解决form表单重复提交方案

 在实际业务系统应用过程中,都会存在一个表单数据重复提交的问题。针对这个问题网上也存在N多种解决方案。
 为节省选择时间,因此在这N多种方案中,我整理了自认为从根本上解决重复提交的问题的一个最优方案。

项目总体概况

具体实现原理自己理解吧,真不想写了,2019年5月17日10:54:54 QingDao LaoShan这个月份 灰蒙蒙的天 冻的俺都不敢伸出手 嘿嘿~~
还是在啰嗦两句吧!
该方案是利用aop注解+redis,每次表单提交都会携带token,在aop中token+path组成一个唯一的key,然后存储到redis中,其中value为系统生成的随机字符串,唯一的不同是就是设定redis中该key的过期时间。redis就是利用这个时间周期,验证是否该key已存在,已存在就不在执行后续操作。
springboot解决form表单重复提交方案_第1张图片

实现明细

pom.xml


	4.0.0

	com.norepeat
	springboot-norepeat
	0.0.1-SNAPSHOT
	jar

	springboot-norepeat
	http://maven.apache.org

	
		org.springframework.boot
		spring-boot-starter-parent
		2.1.3.RELEASE
		 
	

	
		UTF-8
	

	
		
			org.springframework.boot
			spring-boot-starter-data-redis
			
				
					redis.clients
					jedis
				
				
					io.lettuce
					lettuce-core
				
			
		
		
			org.springframework.boot
			spring-boot-starter-web
		
		
			org.springframework.boot
			spring-boot-starter-aop
		
		
			org.springframework.boot
			spring-boot-devtools
			runtime
		
		
			org.springframework.boot
			spring-boot-starter-test
			test
		

		
			redis.clients
			jedis
		

		
			org.apache.commons
			commons-pool2
		
	
	
		
			
				org.springframework.boot
				spring-boot-maven-plugin
			
		
	


application.properties

server.port=8000
# Redis数据库索引(默认为0)
spring.redis.database=0  
# Redis服务器地址
spring.redis.host=192.168.0.192
# 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

App.java

package com.norepeat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;


/**   
 * @ClassName:  App   
 * @Description:TODO(启动入口)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午10:52:10   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */ 
@SpringBootApplication
public class App {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

	@Bean
	public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
		return new RestTemplate(factory);
	}

	@Bean
	public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
		SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
		return factory;
	}

}

ApiResult.java

package com.norepeat;

/**   
 * @ClassName:  ApiResult   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:11:38   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */
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 +
                '}';
    }
}

AOP

NoRepeatSubmit.java

package com.norepeat.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**   
 * @ClassName:  NoRepeatSubmit   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:08:27   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */

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

    /**
     * 设置请求锁定时间
     *
     * @return
     */
    int lockTime() default 10;

}

RepeatSubmitAspect .java

package com.norepeat.aop;

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 com.norepeat.ApiResult;
import com.norepeat.utils.RedisLock;
import com.norepeat.utils.RequestUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
/**   
 * @ClassName:  RepeatSubmitAspect   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:09:12   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */

@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) {
    }

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

    }

    private String getKey(String token, String path) {
        return token + path;
    }

    private String getClientId() {
        return UUID.randomUUID().toString();
    }

}

UTILS

RedisLock.java

package com.norepeat.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;
/**   
 * @ClassName:  RedisLock   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:10:06   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 * Redis 分布式锁实现
 * 如有疑问可参考 @see Redis分布式锁的正确实现方式
 *
 *
 */
@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) 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) 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.java

package com.norepeat.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**   
 * @ClassName:  RequestUtils   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:10:58   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */
public class RequestUtils {

    public static HttpServletRequest getRequest() {
        ServletRequestAttributes ra= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return ra.getRequest();
    }

}

controller

SubmitController.java

package com.norepeat.controller;

/**   
 * @ClassName:  SubmitController   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人 
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:12:15   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.norepeat.ApiResult;
import com.norepeat.aop.NoRepeatSubmit;

@RestController
public class SubmitController {

    @PostMapping("submit")
    @NoRepeatSubmit(lockTime = 30)
    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();
        }
    }

}

test

RunTest.java

package com.norepeat.test;

/**   
 * @ClassName:  RunTest   
 * @Description:TODO(这里用一句话描述这个类的作用)   
 * @author: 寻找手艺人
 * @email:  [email protected] 
 * @date:   2019年5月17日 上午9:13:45   
 *     
 * @Copyright: 2019 www.maker-win.net Inc. All rights reserved. 
 *  
 */
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;

@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 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 body = new HashMap<>();
        body.put("userId", userId);
        return new HttpEntity<>(body, headers);
    }

}

执行效果

springboot解决form表单重复提交方案_第2张图片


※隆重声明

以上代码源码均来自开演社区,寻找手艺人只是搬运到CSDN博客内了,就是方便大家查阅 汗颜~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~寻找手艺人

你可能感兴趣的:(springboot)