你的浏览器不支持canvas

做你害怕做的事情,然后你会发现,不过如此。

使用Spring的AOP切面防止表单重复提交

时间: 作者: 黄运鑫

本文章属原创文章,未经作者许可,禁止转载,复制,下载,以及用作商业用途。原作者保留所有解释权。


使用场景

  • 在前端提交表单时,由于网络卡顿或误操作,用户点击了多次提交按钮,导致新增多条重复数据。
  • 后端可以使用AOP切面防止重复提交,在第一次提交未处理完时,如果再次提交相同数据,则不处理。
  • 此实例比较简单,只能防止同一时间相同数据的重复提交;如果需要防止同一个表单的不同数据提交,则需要修改前端代码,在提交表单时携带一次性的表单凭证来实现。

实例代码

工具类

  • HashUtils工具类,用于计算字符串的MD5值,代码如下:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * @author hyx
 */
public class HashUtils {
    /**
     * 获取字符串的md5值
     */
    public static String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}
  • RedisLockService用于操作redis加锁和解锁,代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;

import java.util.Collections;

/**
 * @author hyx
 */
@Service
public class RedisLockService {
    private static final Long SUCCESS = 1L;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 获取锁
     *
     * @param lockKey
     * @param value
     * @param expireTime 锁有效时间 单位-秒
     * @return
     */
    public boolean getLock(String lockKey, String value, int expireTime) {
        boolean ret = false;
        try {
            String script = "if redis.call('setNx',KEYS[1],ARGV[1]) == 1 then if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end else return 0 end";

            RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);

            Object result = redisTemplate.execute(redisScript, new StringRedisSerializer(),
                    new StringRedisSerializer(), Collections.singletonList(lockKey), value, String.valueOf(expireTime));

            if (SUCCESS.equals(result)) {
                return true;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 释放锁
     *
     * @param lockKey
     * @param value
     * @return
     */
    public boolean releaseLock(String lockKey, String value) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);

        Object result = redisTemplate.execute(redisScript, new StringRedisSerializer(),
                new StringRedisSerializer(), Collections.singletonList(lockKey), value);
        if (SUCCESS.equals(result)) {
            return true;
        }

        return false;
    }
}

切面类

  • 创建ASubmit注解,用作切面的切入点,代码如下:
import java.lang.annotation.*;

/**
 * @author hyx
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ASubmit {
}
  • 切面类实现防重复提交,代码如下:
import com.alibaba.fastjson.JSONObject;
import com.transnal.business.api.ApiException;
import com.transnal.business.api.ReplyCode;
import com.transnal.common.spring.ApplicationContextHolder;
import com.transnal.publish.common.utils.HashUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @author hyx
 */
@Aspect
@Component
public class SubmitAspect {

    //注解切入点
    @Pointcut("@annotation(com.transnal.publish.admin.ASubmit)")
    //方法切入点
    // @Pointcut("execution(public * com.transnal.publish.admin..*Controller.create(..))")
    public void addAdvice() {
    }

    /**
     * 环绕通知
     */
    @Around("addAdvice()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        RedisLockService redisLockService = ApplicationContextHolder.getBean(RedisLockService.class);
        Object[] args = joinPoint.getArgs();
        //计算请求参数的md5
        String md5Key = HashUtils.hashKeyForDisk(JSONObject.toJSONString(args));
        //方法执行前使用参数的md5加锁,如果md5重复则代表重复提交
        boolean lock = redisLockService.getLock(md5Key, md5Key, 30);
        if (lock) {
            try {
                //执行方法
                Object proceed = joinPoint.proceed();
                return proceed;
            } catch (Exception e) {
                e.printStackTrace();
                throw new ApiException(ReplyCode.Error, e.getMessage());
            } finally {
                //执行成功或失败都解锁
                redisLockService.releaseLock(md5Key, md5Key);
            }
        }

        //加锁失败则代表重复提交
        throw new ApiException(ReplyCode.Error, "请不要重复提交");
    }
}

  • 在需要防止重复提交的方法上使用ASubmit注解即可,使用代码如下:
public class SlideController {
    /**
     * 新增接口
     */
    @ASubmit
    @Override
    public String create(Slide slide) {
        //...
    }
}

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。