package com.cku.aop; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Objects; import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHeaders; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; 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.http.MediaType; import org.springframework.stereotype.Component; import com.cku.annotation.MultiSubmit; import com.cku.annotation.MultiSubmitWithToken; import com.cku.core.ZAErrorCode; import com.cku.core.ZAException; import com.cku.restful.v1.sys.utils.MD5Util; import com.thinkgem.jeesite.common.mapper.JsonMapper; import com.thinkgem.jeesite.common.utils.IdGen; import com.thinkgem.jeesite.common.utils.JedisUtils; import com.thinkgem.jeesite.modules.sys.utils.UserUtils; import edu.emory.mathcs.backport.java.util.Arrays; import lombok.extern.slf4j.Slf4j; /** * 重复提交靠按钮解决不是特别靠谱,后台加mysql锁也是不优(今后分布式了), 使用redis分布式锁(setnx)解决 hash值 * 对象实例相同,默认的hash值相等(配合lombok的@Data对hashcode的重写),通过hash值作为锁值校验入参(不要随便重写request * hash()方法) hash值超时时间(默认 5 s) */ @Aspect @Component @Slf4j public class MultiSubmitHandler { /** * 锁的默认时间为5秒 */ public static final int REPEAT_LOCK_TIME = 5; @Pointcut("@annotation(com.cku.annotation.MultiSubmit) || @annotation(com.cku.annotation.MultiSubmitWithToken)") public void pointcut() { } /** * 防止重复提交的请求,请求之前的逻辑 对 用户id + 请求参数的hashcode 加锁 * * @param joinPoint 获取请求Request */ @Before("pointcut()") public void before(JoinPoint joinPoint) { long start = System.currentTimeMillis(); Object[] objects = joinPoint.getArgs(); Annotation multiSubmit = getMethodAnnotation(joinPoint, MultiSubmit.class); int timeout = REPEAT_LOCK_TIME;// 默认5秒 if (multiSubmit instanceof MultiSubmit) { timeout = ((MultiSubmit) multiSubmit).timeout(); } else { timeout = ((MultiSubmitWithToken) multiSubmit).timeout(); } String rdsKey = getRedisKey(joinPoint, objects); if (JedisUtils.setNx(rdsKey, "1", timeout)) { log.info("submitting repeat check time : " + (System.currentTimeMillis() - start) + "ms, key:" + rdsKey); } else { log.error("submitting repeat: " + joinPoint.toLongString()); throw new ZAException(ZAErrorCode.ZA_VALID_QUEST_FREQUENTLY, "重复提交"); } } /** * 防止重复提交的请求,请求之后的逻辑 释放锁 * * @param joinPoint 获取请求Request * @param res 暂无用 */ @AfterReturning(pointcut = "pointcut()", returning = "res") public void afterReturning(JoinPoint joinPoint, Object res) { Object[] objects = joinPoint.getArgs(); String key = getRedisKey(joinPoint, objects); JedisUtils.del(key); log.info("submitting repeat lock released, key:" + key); } /** * 获取防止相同请求重复提交的redis锁的key * * @param objects 请求参数 * @return key redis锁的key */ private String getRedisKey(JoinPoint joinPoint, Object[] objects) { String key = getParamKey(objects); String uid; try { Annotation multiSubmit = getMethodAnnotation(joinPoint, MultiSubmit.class); if (multiSubmit instanceof MultiSubmit) { uid = UserUtils.getLoginUser().getId(); } else { uid = UserUtils.getLoginUser().getTicket(); } } catch (Exception e) { uid = null; } if (StringUtils.isNotBlank(uid)) { key = "R:" + uid + ":" + key; } else { key = "R:" + key; } return key; } @SuppressWarnings("unchecked") public String getParamKey(Object[] objects) { // 随机获取key(保证获取不到参数时,不阻断正常提交),如请求有参数,会根据参数改变 key String resultKey = IdGen.uuid(); HttpServletRequest request = (HttpServletRequest) Arrays.asList(objects).stream() .filter(o -> o instanceof HttpServletRequest).findAny().orElse(null); // 非HttpServletRequest接收参数 if (Objects.isNull(request)) { resultKey = String.valueOf(objects[0].hashCode()); } // 非json类型 else if (request.getHeader(HttpHeaders.CONTENT_TYPE) == null || !request.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) { // 普通参数 if (Objects.nonNull(request.getParameterMap()) && !request.getParameterMap().isEmpty()) { String reqStr = JsonMapper.toJsonString(request.getParameterMap()); resultKey = MD5Util.MD5Encode(StringUtils.isEmpty(reqStr) ? "" : reqStr, Charsets.UTF_8.toString()); } // json类型 } else { // 从输入流中取出body串, 如果为空,直接返回 try { String reqBodyStr = IOUtils.toString(request.getInputStream(), Charsets.UTF_8); resultKey = MD5Util.MD5Encode(StringUtils.isEmpty(reqBodyStr) ? "" : reqBodyStr, Charsets.UTF_8.toString()); } catch (IOException e) { e.printStackTrace(); } } return resultKey; } /** * 获取方法注解 * * @param joinPoint * @param clazz * @param * @return */ private T getMethodAnnotation(JoinPoint joinPoint, Class clazz) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); return method.getDeclaredAnnotation(clazz); } }