java防止重复提交解决方案

    技术2022-07-16  90

    java开发防止重复提交问题

    问题描述解决思路代码解释

    问题描述

    1.在我们项目开发过程中会出现用户保存操作时候快速点击两次会出现一条数据在数据库保存多条数据。

    2.遇见上述问题我们首先跟前端开发沟通,在前端开发过程中可以将操作按钮在操作完后就行置灰操作,虽然这样做了,但是不能从根们解决问题(前端和后端都防止,这样就可以根本解决问题),然后需要后端进行提交接口进行重复提交处理。

    解决思路

    1.前端利用js操作或者vue操作进行按钮置灰,防止二次点击! 2.java后端利用redis进行防止重复操作!

    每次请求提交保存数据需要提前请求接口获取token,然后在提交请求将获取token放入需要提交数据的接口头部,然后将保存操作接口放入注解@ExtApiIdempotent(ConstantUtils.EXTAPIHEAD),后端会先进入aop中在头部找到token来判断redis是否存在当前redis,如果存在说明没有重复然后进行删除redis缓存数据,如果你连续点击第二次时候其实这个token已经被上一个请求给删除掉了,所以就是重复提交,不明白可以在下面评论,我都会回复

    代码解释

    1.在前端请求接口之前先请求获取一个token,我们java端将token生成完返给前端后,放入redis中,token作为key和token作为value 设置失效时间为200秒。

    @RequestMapping("/redisToken") public String getRedisToken() { return token.getToken(); }

    这是token工具类

    package com.example.test.framework.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.UUID; @Component public class RedisToken { @Autowired private BaseRedisService baseRedisService; @Autowired private RedisUtil redisUtil; /** 缓存指定时间200秒 */ private static final long TOKENTIMEOUT = 200; /** * 生成Token */ public String getToken(){ String token = UUID.randomUUID().toString(); token=token.replaceAll("-", ""); // 将token放到Redis中,用UUID保证唯一性 baseRedisService.setString(token, token, TOKENTIMEOUT); return token; } public synchronized boolean findToken(String tokenKey) { String tokenValue = (String) redisUtil.get(tokenKey); // 如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑 if (StringUtils.isEmpty(tokenValue)) { return false; } // 保证每个接口对应的token 只能访问一次,保证接口幂等性问题,用完直接删掉 redisUtil.delete(tokenValue); return true; } }

    2.前端获取到token后,然后每次操作接口时候需要将token放入每次请求head中,后端需要利用AOP面向切面对请求token进行校验

    package com.example.test.framework.annotion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 带参数的,一般在前后端分离时候使用,就可以将token放入请求头部,就可以直接在接口处设置ConstantUtils.EXTAPIHEAD类型就可以了 */ //RetentionPolicy.RUNTIME:注解保留在程序运行期间,此时可以通过反射获得定义在某个类上的所有注解. //ElementType.METHOD:说明该注解只能被声明在一个类的方法前。 @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExtApiIdempotent { //也就是接口中注解携带的参数,在我这里用的是请求方式类型 举例:@ExtApiIdempotent(ConstantUtils.EXTAPIHEAD) String value(); } package com.example.test.framework.annotion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 不带参数的一般在前后端没有分离,我们假如在controller跳转某个页面,然后在页面进行form表单提交,那就需要在跳转页面接口用@ExtApiToken这个注解,然后在页面用<input type="hidden" name="token" value="${token}">将token放入form标签内哦 */ @Target(value = ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExtApiToken { } package com.example.test.framework.aop; import com.example.test.framework.annotion.ExtApiIdempotent; import com.example.test.framework.annotion.ExtApiToken; import com.example.test.framework.redis.RedisToken; import com.example.test.framework.redisservice.ConstantUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class ExtApiAopIdempotent { @Autowired private RedisToken redisToken; // 1.使用AOP环绕通知拦截所有test下面某某文件下controller的方法 // 2.这里是你需要拦截的controller位置,设置一个我们上层公共位置可以了, @Pointcut("execution(* com.example.test.*.controller.*.*(..)))") public void rlAop() { } /** * 封装数据-在请求连接中获取需要的参数,例如:请求头中token */ public HttpServletRequest getRequest() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; } /** * 前置通知-不带参数的一般在前后端没有分离,我们假如在controller跳转某个页面,然后在页面进行form表单提交,那就需要在跳转页面接口用@ExtApiToken这个注解,然后在页面用<input type="hidden" name="token" value="${token}">将token放入form标签内哦 */ @Before("rlAop()") public void before(JoinPoint point) { // 获取被增强的方法相关信息 - 查看方法上是否有次注解 MethodSignature signature = (MethodSignature) point.getSignature(); ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class); if (extApiToken != null) { // 可以放入到AOP代码 前置通知 getRequest().setAttribute("token", redisToken.getToken()); } } /** * 环绕通知 */ @Around("rlAop()") public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 获取被增强的方法相关信息 - 查看方法上是否有次注解 MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature(); ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class); if (declaredAnnotation != null) { String values = declaredAnnotation.value(); String token = null; HttpServletRequest request = getRequest(); if (values.equals(ConstantUtils.EXTAPIHEAD)) { token = request.getHeader("token"); } else { token = request.getParameter("token"); } // 获取不到token if (StringUtils.isEmpty(token)) { return "获取不到token"; } // 接口获取对应的令牌,如果能够获取该(从redis获取令牌)令牌(将当前令牌删除掉) 就直接执行该访问的业务逻辑 boolean isToken = redisToken.findToken(token); // 接口获取对应的令牌,如果获取不到该令牌 直接返回请勿重复提交 if (!isToken) { return "请勿重复提交!"; } } Object proceed = proceedingJoinPoint.proceed(); return proceed; } }

    设置请求属于哪种类型可以是http-from两种请求

    package com.example.test.framework.redisservice; public interface ConstantUtils { /** * http 中携带的请求 */ static final String EXTAPIHEAD = "head"; /** * from 中提交的请求 */ static final String EXTAPIFROM = "from"; }

    controller层代码-如果你不是from表单提交就不要考虑ConstantUtils.EXTAPIFROM参数

    package com.example.test.systemmo.controller; import com.example.test.framework.annotion.ExtApiIdempotent; import com.example.test.framework.annotion.ExtApiToken; import com.example.test.framework.redis.RedisToken; import com.example.test.framework.redisservice.ConstantUtils; import com.example.test.systemmo.pojo.SysLcfLog; import com.example.test.systemmo.service.imp.SysLcfLogService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Array; import java.util.ArrayList; /** * <p> * 操作日志 前端控制器 * </p> * * @author lcf * @since 2020-04-24 */ @RestController @RequestMapping("/systemmo/sss") public class SysLcfLogController { @Autowired private SysLcfLogService syslcflogservice; @Autowired private RedisToken token; @RequestMapping("/redisToken") public String getRedisToken() { return token.getToken(); } /** * http请求增加日志 * @param syslsflog * @return */ @RequestMapping("/add") @ExtApiIdempotent(ConstantUtils.EXTAPIHEAD) public Object add(SysLcfLog syslsflog){ //你的业务逻辑操作 return "ok"; } /** * 跳转页面,我们需要将token给你在跳转时候传送给你的页面,这样就可以在form表单提交时候直接传入给fromAdd接口 * @param req * @return */ @RequestMapping("/logPage") @ExtApiToken public String logPage(HttpServletRequest req) { return "logPage"; } /** * form增加日志 * @param syslsflog * @return */ @RequestMapping("/fromAdd") @ExtApiIdempotent(ConstantUtils.EXTAPIFROM) public Object fromAdd(SysLcfLog syslsflog){ //你的业务逻辑操作 return "ok"; } @RequestMapping("/select") public Object select(){ ArrayList<SysLcfLog> log=new ArrayList<SysLcfLog>(); log=syslcflogservice.select(); return log; } }

    防止重复提交进行根据http请求和from表单提交进行总结分析,遇见问题时候也是在网上找资料,总结成自己的语言分享给大,大家哪里不懂,我会一一回复,后期会更新更多硬货,都是在实战项目中遇见的

    Processed: 0.014, SQL: 9