AOP拦截重复请求

151 阅读2分钟

总体思路:AOP拦截特定的注解,根据参数以及请求方法生成特定的md5字符串,在固定时间内存在即判断为重复请求,返回前台错误信息

AOP代码

获取参数注解要注意下

package com.marry.simplespringboot.aspect;


import com.marry.simplespringboot.annotation.ExcludeRepeat;
import com.marry.simplespringboot.annotation.IncludeRepeat;
import com.marry.simplespringboot.annotation.RepeatSubmit;
import com.marry.simplespringboot.exception.RepeatSubmitException;
import com.marry.simplespringboot.util.JacksonUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author mal
 * @date 2022-07-26 15:17
 */
@Aspect
@Component
public class RepeatSubmitAspect {
    private static final Logger logger = LoggerFactory.getLogger(RepeatSubmitAspect.class);
    @Autowired
    StringRedisTemplate stringRedisTemplate;




    @Before("@annotation(rs)")
    public void checkReSubmit(JoinPoint joinPoint, RepeatSubmit rs) {

        try {
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            int interval = annotation.interval();
            String message = annotation.message();
            boolean onlyInclude = annotation.onlyInclude();
            Object[] args = joinPoint.getArgs();

            String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
            Parameter[] parameters = method.getParameters();
            Annotation[][] parameterAnnotations = method.getParameterAnnotations();

            List<Object> candidateArgList = new ArrayList<>();
            candidateArgList.add(getMethodUrl(method));
            if (args != null) {
                for (int i = 0; i < parameters.length; i++) {
                    String parameterName = parameterNames[i];
                    Object arg = args[i];
                    Annotation[] annotations = parameterAnnotations[i];
                    if (onlyInclude) {
                        final IncludeRepeat includeRepeat =
                                getAnnotationByType(annotations, IncludeRepeat.class);
                        if (includeRepeat == null) {
                            continue;
                        }
                    } else {
                        final ExcludeRepeat excludeRepeat =
                                getAnnotationByType(annotations, ExcludeRepeat.class);
                        if (excludeRepeat != null || ignoreArg(arg, parameterName)) {
                            continue;
                        }
                    }
                    candidateArgList.add(arg);

                }
            }


            String lockKey = DigestUtils.md5Hex(JacksonUtil.writeObj2Json(candidateArgList));

            //boolean flag = memcachedClient.add("REPEAT_SUBMIT/" + lockKey, interval, System.currentTimeMillis());
            //if (!flag) {
            //    throw new RepeatSubmitException(message);
            //}


            boolean lock = RepeatSubmitLock.getInstance().lock(lockKey, System.currentTimeMillis(), interval);
            if (!lock) {
                throw new RepeatSubmitException(message);
            }

            //boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("repeat_submit:" + lockKey, String.valueOf(System.currentTimeMillis()), interval, TimeUnit.MILLISECONDS);
            //if (!lock) {
            //    throw new RepeatSubmitException(message);
            //}
        } catch (RepeatSubmitException e) {
            throw e;
        } catch (Exception e) {
            logger.error("checkReSubmit,error", e);
        }


    }

    private static <T extends Annotation> T getAnnotationByType(final Annotation[] annotations,
                                                                final Class<T> clazz) {
        T result = null;
        for (final Annotation annotation : annotations) {
            if (clazz.isAssignableFrom(annotation.getClass())) {
                result = (T) annotation;
                break;
            }
        }
        return result;
    }

    private String getMethodUrl(Method method) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            if (request != null) {
                return request.getRequestURI();
            }
        }
        return method.getDeclaringClass().getName() + "_" + method.getName();
    }

    private boolean ignoreArg(Object param, String parameterName) {
        if (param instanceof HttpServletRequest
                || param instanceof HttpServletResponse
                || param instanceof MultipartFile
                || param instanceof MultipartFile[]
                || param instanceof ModelMap
                || param instanceof Model
                || param instanceof byte[]) {
            return true;
        }
        if (parameterName.contains("base64") || parameterName.contains("Base64") || parameterName.contains("agreement")) {
            return true;
        }

        return false;
    }


}

本来系统中是用的memcache,它的add 方法也类似 redis.setnx方法了,但是大家现在用redis的比较多,可以把缓存放到redis中,测试时就直接放map中。

记录请求的MAP

concurrentHashMap存储生成的key, 用延时任务线程池来指定的时间内删除存储,达到过期效果

package com.marry.simplespringboot.aspect;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author mal
 * @date 2022-07-26 16:19
 */
public class RepeatSubmitLock {
    private static final Logger logger = LoggerFactory.getLogger(RepeatSubmitLock.class);

    private final ConcurrentHashMap<String, Object> LOCK_CACHEMAP = new ConcurrentHashMap<>(100);


    private final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.CallerRunsPolicy());


    private RepeatSubmitLock() {

    }

    private static class SingletonInstance {
        private static final RepeatSubmitLock INSTANCE = new RepeatSubmitLock();

    }

    public static RepeatSubmitLock getInstance() {
        return SingletonInstance.INSTANCE;
    }


    public boolean lock(final String key, Object value, int intervals) {
        Object absent = LOCK_CACHEMAP.putIfAbsent(key, value);
        if (Objects.isNull(absent)) {
            logger.info("lock,set value for key 【{}】",key);
            EXECUTOR.schedule(() -> {
                        logger.info("lock,clear value  for key 【{}】", key);

                        LOCK_CACHEMAP.remove(key);
                    }
                    , intervals, TimeUnit.MILLISECONDS);
            return true;
        } else {
            if ((((Long) value) - ((Long) absent))> intervals) {
                LOCK_CACHEMAP.put(key, value);
                EXECUTOR.schedule(() -> LOCK_CACHEMAP.remove(key), intervals, TimeUnit.MILLISECONDS);
                return true;
            }


        }
        return false;
    }


}

注解内容

三个注解的内容很简单

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
    /**
     * 判断是重复请求的间隔时间
     * 单位 秒
     * @return
     */
    int interval() default 10000;

    String message() default "请勿重复提交!";

    /**
     * 是否只判断包含@InCludeRepeat注解的数据
     * @return
     */
    boolean onlyInclude() default false;

}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IncludeRepeat {
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExcludeRepeat {
}

简单的请求

@RestController
@RequestMapping("/repeat")
public class RepeatController {

    @RepeatSubmit
    @RequestMapping("/sayHello")
    public String sayHello(String name) {
        return "hello" + name;
    }

日志

可以看出来时间还是听准确的

2022-07-28 16:36:03.509lockset value for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:13.525   : lock,clear value  for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:13.558   : lockset value for key 【3832e882eb227e3c5de35a569304287a】
2022-07-28 16:36:23.561   : lock,clear value  for key 【3832e882eb227e3c5de35a569304287a】

总结

有条件的话还是建议使用redis来做,性能好,也省得占用应用服务器的资源