使用场景
- 在前端提交表单时,由于网络卡顿或误操作,用户点击了多次提交按钮,导致新增多条重复数据。
- 后端可以使用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) {
//...
}
}