java学习笔记之: 重复提交解决方案

一.场景:
a.一个按钮在未响应前多次点击 常见为 网络慢造成(如双11 购买按钮)
b.响应完成后再次提交相同数据,属于正常的请求,是后台防止数据库数据重复的问题,那是另外一个故事了
c.提交后页面上的url是要提交的目标链接 显示是servlet 然后刷新/后退 会造成重复提交
(现在基本用Ajax,就不会有这个问题,这种场景涉及技术很老 基本不会出现了 所以忽略吧)

所以针对第一个场景:
二.方案
1.前台方案:标记flag
提交一次提交后就不再提交/ 外观可见为按钮不允许再点 比较简单

2.后台方案:利用session
这里写图片描述
a.在这些关键词中,我们用什么来避免场景a呢?
session + request,每次点击会生成新的request,其传递数据是相同的,但其是共享session的,不同的request共享一个session,目的是只让其中一个request有效,遵循这个思路,可以再request中放一个token,session里也放一个,然后多个request中只有一个token和session对比有效 ,似乎就可以防止到了
这里写图片描述
b.在此场景下衍生出一个场景问题:同一个浏览器打开两个窗口, 窗口间也是共享session的,那session改了窗口的正常提交场景都有可能是报错的

那就多存几个session里token值,由String存改为list存,可以限制list大小以及token存在时间,同时可以搞定表单超时问题

c.but request中的token怎么来,表单生成前就需要先经过后台交互生成token值,好麻烦

1.jsp可以直接搞定 jsp页面直接生成request/session中的token
2.html就不行了,那是否不生成session而直接交互,然后第一次请求时session里null,接着添加session的值,那第二次请求session里就不为null了  好像这样的逻辑可行-->but 经验证,什么时候添加session里的token都有问题,不行

d.最后一个问题 session是在处理业务前还是后清除呢

    很明显 ,若在业务后清除,等第一个请求处理完业务然后去清除session的时间,可能其他请求已经验证完成了,就验证不到了,所以 得在验证完后就 立即清除,确保不会验证有效性

请注意 以上方案针对的场景是弱网络状况的下的多次点击 即场景a
代码呢 也只是本地跑着玩,实际开发没用上 有问题我不负责的 谢谢
遗留问题就是谁能搞定方案c中提到的request中的token怎么样可以不交互后台而直接得到

核心代码如下:
切面切自定义注解
生成代码的url那里@FormRepeat(getForm=true)
其他需要校验重复性的加上注解@FormRepeat

  package com.pafa.testDemo.from;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;

import jdk.nashorn.internal.parser.TokenStream;

import org.apache.commons.lang3.StringUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.google.common.cache.Cache;
import com.pafa.testDemo.baseParam.BaseJsonObjectResult;
import com.palic.elis.ius.web.param.base.ReturnResult;

/**
 * AOP实现 /拦截器实现 
 * 名词解释: 
 * 连接点joinpoint 当前连接的点 正在被切的点 具体的方法之类的
 * 切入点pointcut 何地(某些方法/某一类注解)
 * 通知advice 何时(在方法前还是方法后还是异常后)
 * 
 * @author EX-ZHOUXIAOWEI004
 *
 */
@Component
@Aspect
public class FormAspect {

    private Logger log = LoggerFactory.getLogger(FormAspect.class);

    private static List<String> tokens = new ArrayList<String>();
    // 切入点 这个注解
    @Pointcut("@annotation(com.pafa.testDemo.from.FormRepeat)")
    public void token() {

    }

    // 环绕通知 对应切入点是token
    @Around("token()")
    public Object aroundToken(ProceedingJoinPoint joinpoint) {
        Object object = null;

        String tokenName = "token";

        try {
            System.out.println("AOP环绕通知start");
            //获取request
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();

            // 获取formRepeat注解的值
            MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();

            Method method = methodSignature.getMethod();
            FormRepeat formRepeat = method.getAnnotation(FormRepeat.class);

            boolean getForm = formRepeat.getForm();
            if (getForm) {
                generate(request, tokenName);
                object = joinpoint.proceed();
            } else {
                // 开始校验
                if (verify(request, tokenName)) {
                    object = joinpoint.proceed();

                }else{
                    //正式使用不做响应即可  否则返回值必须和对应接口的返回值相同
                    return BaseJsonObjectResult.getJsonObject("00004", "无效或者重复提交表单");
                }
            }

        } catch (Throwable e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // 不干涉返回值 直接返回
        return object;
    }

    /**
     * 重置session里先有的值
     * 
     * @param request
     */
    public static String generate(HttpServletRequest request, String tokenName) {
        String token = System.currentTimeMillis() + "" + new Random().nextInt(555);
        tokens.add(token);

        //只允许有两个
        if(tokens.size() > 2){
            tokens.remove(0);
        }
        System.out.println("生成的Token:" + token);
        System.out.println("生成的Tokens:" + tokens);

        request.getSession().setAttribute(tokenName, tokens);

        return token;
    }

    /**
     * 校验token
     * 
     * @param request
     */
    public static Boolean verify(HttpServletRequest request, String tokenName) {
        System.out.println("校验Token:" + tokenName);

        Boolean result = false;
        List sessionToken = (List) request.getSession().getAttribute(tokenName);
        String requestToken = (String) request.getParameter(tokenName);

        // 对比token&& session
        // 如果request没有token 检验不过
        if (StringUtils.isEmpty(requestToken)) {
            result =  false;

            return result;
        }

        // 对比session OK
        if (sessionToken.contains(requestToken)) {
            result =  true;
        }

        // 检验完后 不论结果 立即再次生成新的 不等处理业务
        generate(request, tokenName);

        return result;

    }

}

控制器代码

@Controller
public class FormSolution {

    //提交表单的接口
    @RequestMapping("/submitForm")
    @ResponseBody
    @FormRepeat
    public JSONObject submitForm(HttpServletRequest request,String form){

        System.out.println("业务begin");

        try {
            Thread.currentThread().sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("业务end");

        return BaseJsonObjectResult.getSuccessJsonObject(form);
    }

    //生成表单token的接口
    @RequestMapping("/getForm")
    @ResponseBody
    @FormRepeat(getForm=true)
    public JSONObject getForm(HttpServletRequest request){

        return BaseJsonObjectResult.getSuccessJsonObject();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值