叙述

平时开发的项目中可能会出现下面这些情况:

  1. 由于用户误操作,多次点击表单提交按钮。
  2. 由于网速等原因造成页面卡顿,用户重复刷新提交页面。
  3. 黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。

这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。

实现原理:

  1. 自定义防止重复提交标记(@AvoidRepeatableCommit)。
  2. 对需要防止重复提交的Congtroller里的mapping方法加上该注解。
  3. 新增Aspect切入点,为@AvoidRepeatableCommit加入切入点。
  4. 每次提交表单时,Aspect都会保存当前key到reids(须设置过期时间)。
  5. 重复提交时Aspect会判断当前redis是否有该key,若有则拦截。

解决方案

自定义标签:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 避免重复提交
 * @ClassName: AvoidRepeatableCommit
 * @date: 2019/3/26 16:38
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {

    /**
     * 指定时间内不可重复提交,单位毫秒
     */
    long timeout()  default 3000 ;

}

自定义切入点Aspect:

import com.ccx.ticket.Constants;
import com.ccx.ticket.util.HttpUtil;
import com.ccx.ticket.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 重复提交aop
 * @data: 2019-03-26 16:39
 **/
@Aspect
@Component
@Slf4j
public class AvoidRepeatableCommitAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @param point
     */
    @Around("@annotation(com.ccx.ticket.filter.AvoidRepeatableCommit)")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        HttpServletRequest request  = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String ip = HttpUtil.getIpAddr(request);
        //获取注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //目标类、方法
        String className = method.getDeclaringClass().getName();
        String name = method.getName();
        String ipKey = String.format("%s#%s",className,name);
        int hashCode = Math.abs(ipKey.hashCode());

        String key = Constants.REDIS_ROOT+String.format("%s_%d",ip,hashCode);

        log.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
        AvoidRepeatableCommit avoidRepeatableCommit =  method.getAnnotation(AvoidRepeatableCommit.class);
        long timeout = avoidRepeatableCommit.timeout();
        if (timeout < 0){
            timeout = 3*Constants.SECOND;
        }
        String value = (String) redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)){

            return "请勿重复提交";
        }
        redisTemplate.opsForValue().set(key, UUID.randomUUID().toString(),timeout, TimeUnit.MILLISECONDS);
        //执行方法
        Object object = point.proceed();

        return object;
    }

}

常量类:

public class Constants {

	public static final String REDIS_ROOT="WX:ccx-:";

	public static final long DAY = 1000L*60*60*24;

	public static final long HOUR = 1000L*60*60;

	public static final long MINUTE = 1000L*60;

	public static final long MINUTE_10 = 1000L*60*10;

	public static final long HALF_HOUR = 1000L*60*30;

	public static final long SECOND = 1000L;
}

使用方法:

@AvoidRepeatableCommit
    @RequestMapping(value = "/search", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.POST)
    @ResponseBody
    public ResponseMsg<Map> preSearch(@RequestBody(required = false) Map<String, Object> param) {

       //具体处理业务

    return null;
}

效果图:

redis 重复消费 redis防止重复提交_java