首页 > 其他分享 >利用aop、拦截器HandlerInterceptor来实现接口限流,日志收集

利用aop、拦截器HandlerInterceptor来实现接口限流,日志收集

时间:2024-01-20 22:08:09浏览次数:31  
标签:拦截器 return class 获取 限流 aop 注解 public


前言:aop是面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。拦截器是web请求中一个请求周期中的一环

就实现接口限流这个需求来说,用aop和HandlerInterceptor都可以来实现,就是在调用接口之前做一些约束而已。

aop+自定义注解+Semaphore实现接口限流

自定义限流注解

/**
 * @author 张子行
 * @class 限流注解,秒级访问多少次
 */
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(value = ElementType.METHOD)
public @interface Limit {
    //访问次数
    int maxLimit() default 10;
    //周期
    int cycle() default 1;
}

就是利用Semaphore+aop实现对加了limit注解的方法进行限流拦截的一个切面,相关知识参考注解版的springaop实操讲解(赋完整测试代码)

/**
 * @author 张子行
 * @class 限流切面
 */
@Aspect
@Component
@Slf4j
public class limitAspect {
    private ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.zzh.currentlimiting.aspects.limit.Limit)")
    public void limit() {

    }

    /**
     * @param
     * @method 对接口进行限流
     */
    @Around("limit()")
    public R before(ProceedingJoinPoint joinPoint) throws NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //获取被该注解作用的对象
        Object target = joinPoint.getTarget();

        //获取被该注解作用的对象名字
        String targetName = target.getClass().getName();
        //获取被该注解作用的对象的class
        Class<?> aClass = target.getClass();
        //获取请求的参数
        Object[] methodParam = joinPoint.getArgs();
        //获取被该注解作用的方法的名字
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        StringBuffer bufferKey = new StringBuffer().append(methodName).append(targetName);
        String key = String.valueOf(bufferKey);
        Method[] methods = aClass.getMethods();
        Limit annotation = null;
        //遍历所有的方法
        for (Method method : methods) {
            //根据获取到的方法名字,匹配获取该方法
            if (methodName.equals(method.getName())) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                //方法中的参数匹配,精确匹配方法
                if (parameterTypes.length == args.length) {
                    annotation = method.getAnnotation(Limit.class);
                }
            }
        }
        if (null != annotation) {
            Semaphore semaphore = semaphores.get(key);
            if (null == semaphore) {
                //semaphores.put()
                //初始化各个接口的访问流量
                System.out.println("maxLimit:" + annotation.maxLimit());
                semaphores.putIfAbsent(String.valueOf(key), new Semaphore(annotation.maxLimit()));
                semaphore = semaphores.get(key);
            }
            try {
                //当达到最大的访问的流量后,只有等有空闲的流量时,别的人才能加入
                if(semaphore.tryAcquire()){
                    //执行方法
                    joinPoint.proceed();
                    log.info("成功");
                    return R.ok();
                }
                log.error(methodName+"限流");
                return R.error();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
        log.info("成功");
        return R.ok();
    }

}

测试controller

/**
 * @author 张子行
 * @class 测试限流,日志Controller
 */
@RestController
@RequestMapping
public class logController {
    @Autowired
    com.zzh.currentlimiting.service.impl.pointCutImpl pointCutImpl;
    
    @PostMapping("/login")
    @SysLog(description = "用户登录接口")
    @Limit(maxLimit = 10,cycle = 1)
    public R login(Person person, HttpServletRequest servletRequest) {
        pointCutImpl.say();
        return R.ok();
    }

    @PostMapping("/login2")
    @SysLog(description = "用户登录接口2")
    @Limit(maxLimit = 5,cycle = 1)
    public R login2(Person person, HttpServletRequest servletRequest) {
        pointCutImpl.say();
        return R.ok();
    }
}

HandlerInterceptor+自定义注解+redis实现接口限流

其中HandlerMethod不懂的可以参考这个博客。简单来说HandlerMethod就是可以获取到当前controller这个bean,还有里面的接口名字啊等等,其中主要逻辑就是,对于访问一些加了我们的limit注解的接口,需要做拦截检查,看是否访问量超过了,注解上面所规定的。没有则放行,有则 response写入响应(请求繁忙)给浏览器。

/**
 * @author 张子行
 * @class 限流拦截器
 */
public class currentLimitingInterceptor implements HandlerInterceptor {public class currentLimitingInterceptor implements HandlerInterceptor {
    @Autowired
    private StringRedisTemplate redisTemplate;
    /**
     * @param
     * @method return true放行 return false拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            //没有Limit注解则执行下面的拦截逻辑
            if (!method.isAnnotationPresent(Limit.class)) {
                log.info("没有加limit注解放行");
                return true;
            }
            Limit annotation = handlerMethod.getMethod().getAnnotation(Limit.class);
            //没有加Limit注解的方法就放行
            if (annotation == null) {
                log.info("limit为null放行");
                return true;
            }
            Integer maxLimit = annotation.maxLimit();
            Integer cycle = annotation.cycle();
            Integer rMaxLimit = null;
            String key = method.getName();
            String value = redisTemplate.opsForValue().get(key);
            if (StrUtil.isNotEmpty(value)) {
                rMaxLimit = Integer.valueOf(value);
            }
            //第一此访问此接口,设置初始访问次数为1
            if (rMaxLimit == null) {
                redisTemplate.opsForValue().set(key, "1", cycle, TimeUnit.SECONDS);
            }
            //如果访问次数没有达到Limit注解上标注的最大访问次数,设置访问次数++
            else if (rMaxLimit <= maxLimit) {
                redisTemplate.opsForValue().set(key, rMaxLimit + 1 + "", cycle, TimeUnit.SECONDS);
            }
            //其他的情况,表明需要做限流了,返回一些提示信息
            else {
                ServletOutputStream outputStream = null;
                try {
                    response.setHeader("Content-type", "application/json; charset=utf-8");
                    log.warn("请稍后尝试");
                    response.getWriter().append("");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return false;
            }
        }
        log.info("放行");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

坑springboot 的拦截器中redisTemplate 为null 解决办法

由于拦截器执行在bean实例化前执行的,那么我们就让拦截器执行的时候实例化拦截器Bean,在拦截器配置类里面先实例化拦截器,然后再获取。

/**
 * @author 张子行
 * @class
 */
@Configuration
public class myMvcConfig implements WebMvcConfigurer {
    @Bean
    public currentLimitingInterceptor getCurrentLimitingInterceptor() {
        return new currentLimitingInterceptor();
    }

    @Bean
    public currentLimiting2 getCurrentLimiting2Interceptor() {
        return new currentLimiting2();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getCurrentLimitingInterceptor());
    }
}

我个人感觉这种方式的限流可能不是很精确,毕竟不是原子操作。

HandlerInterceptor+自定义注解+RateLimiter实现接口限流

引入依赖

<!--guava限流 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>

RateLimiter 方法解析

create(int permits) 创建每秒发放permits个令牌的桶

acquire() 不带参数表示获取一个令牌.如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0

acquire(int permits ) 获取permits个令牌,.如果没有获取完令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0

tryAcquire() 尝试获取一个令牌,立即返回(非阻塞)

tryAcquire(int permits) 尝试获取permits 个令牌,立即返回(非阻塞)

tryAcquire(long timeout, TimeUnit unit) 尝试获取1个令牌,带超时时间

tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获取permits个令牌,带超时时间

限流拦截器

/*
 * @author 张子行
 * @class
 */
@Slf4j
public class currentLimiting2 implements HandlerInterceptor {
    //每一秒生成俩个令牌
    private RateLimiter rateLimiter = RateLimiter.create(2, 1, TimeUnit.SECONDS);

    /**
     * @param
     * @method return true放行 return false拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (rateLimiter.tryAcquire()) {
            //获取到令牌可以直接放行
            log.info("放行");
            return true;
        }
        //TODO 可以执行自己的拦截逻辑
        log.warn("拦截");
        return false;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        //执行完方法体才会执行这里的逻辑
        log.info("postHandle");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

aop+自定义注解实现日志收集

其实叭用拦截器也能大概实现,无非就是在请求接口前记录一些信息到数据库。
自定义日志注解

/**
 * @author 张子行
 * @class
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD) //表明这个注解是作用在方法上的
public @interface SysLog {
    //描述信息
    String description() default "";
}

日志切面

/**
 * @author 张子行
 * @class
 */
@Aspect
@Component
@Slf4j
public class logAspect {
    @Pointcut("@annotation(com.zzh.currentlimiting.aspects.log.SysLog)")
    public void controllerLog() {
    }

    @Before("controllerLog()")
    public void before(JoinPoint joinPoint) {
        /**
         * TODO 这里可以收集用户的一些ip,name等信息,然后插入数据库
         */
        saveLog(joinPoint);
    }

    /**
     * @param
     * @method 保存日志信息
     */
    private Boolean saveLog(JoinPoint joinPoint) {
        //获取对应的controller名字
        String controllerName = joinPoint.getTarget().getClass().getName();
        //从servlet中获取一些信息
        ServletRequestAttributes servlet = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String username = servlet.getRequest().getParameter("username");
        String description = null;
        //获取拦截的方法名
        Signature sig = joinPoint.getSignature();
        MethodSignature msig = null;
        Method currentMethod = null;
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        msig = (MethodSignature) sig;
        Object target = joinPoint.getTarget();
        try {
            //获取当前作用的方法
            currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        description = currentMethod.getAnnotation(SysLog.class).description();
//        //获取切面作用的目标类的class
//        Class<?> aClass = joinPoint.getTarget().getClass();
//        //获取作用方法的名字
//        String methodName = joinPoint.getSignature().getName();
//        //根据class获取所有该class下的方法
//        Method[] methods = aClass.getMethods();
//        //获取作用方法上的参数
//        Object[] args = joinPoint.getArgs();
//        //遍历所有的方法
//        for (Method method : methods) {
//            //根据获取到的方法名字,匹配获取该方法
//            if (methodName.equals(method.getName())) {
//                Class<?>[] parameterTypes = method.getParameterTypes();
//                //方法中的参数匹配,精确匹配方法
//                if (parameterTypes.length == args.length) {
//                    description = method.getAnnotation(SysLog.class).description();
//                }
//            }
//        }
        log.info("日志收集:" + username + "请求了" + controllerName + "下面的" + description);
        return true;
    }
}

测试

完整代码点此处利用jmeter开200个线程请求接口,只有少许请求能通过,达到了限流的目的

利用aop、拦截器HandlerInterceptor来实现接口限流,日志收集_后端

利用aop、拦截器HandlerInterceptor来实现接口限流,日志收集_java_02


标签:拦截器,return,class,获取,限流,aop,注解,public
From: https://blog.51cto.com/u_16414043/9346253

相关文章

  • 注解版的springaop实操讲解(赋完整测试代码)
    aop是个很强的东西,我们可以用来实现日志收集,鉴权,敏感词过滤等等功能。在说注解版的springaop使用之前,一些专业术语我用大白话来复述一遍,希望大家不要嫌弃。切面:切入点+通知连接点:目标对象中被增强的某个方法切点:连接点的集合目标对象:被增强的对象织入:把代理逻辑加入到目标对象的过......
  • 利用aop实现热拔插(类似于插件)
    现在有这么一个需求:就是我们日志的开与关是交给使用人员来控制的,而不是由我们开发人员固定写死的。大家都知道可以用aop来实现日志管理,但是如何动态的来实现日志管理呢?aop源码中的实现逻辑中有这么一个步骤,就是会依次扫描Advice的实现类,然后执行。我们要做的就是自定义一个advice的......
  • spring--AOP通知类型有哪些
    SpringAOP(Aspect-OrientedProgramming,面向切面编程)提供了五种类型的通知(advice),这些通知定义了切面(aspect)是在目标对象的方法执行的哪个点被应用。以下是这五种通知类型:前置通知(Beforeadvice):在目标方法执行之前执行的通知,无法阻止方法的继续执行(除非它抛出一个异常)。后置......
  • spring--AOP的实现原理
    SpringAOP(面向切面编程)是Spring框架的一个关键组成部分,它提供了一种将横切关注点(如日志记录、事务管理、安全等)与业务逻辑分离的方法。SpringAOP的实现主要依赖于代理模式,以下是其工作原理的简要概述:代理模式:SpringAOP通过创建目标对象的代理来应用横切逻辑。这些代......
  • AOP 编程
    AOP编程目录AOP编程1.AOP和OOP2.AOP中的一些概念术语通知类型3.AOP实现方式4.AOP使用1.添加依赖:在项目的pom.xml文件中添加SpringAOP依赖,以确保AOP模块可用。2.创建切面类:创建一个Java类,并使用@Aspect注解标记它,这个类将充当切面。1.AOP和OOPAOP为AspectOrie......
  • Spring AOP原来是这样实现的
    SpringAOP技术实现原理在Spring框架中,AOP(面向切面编程)是通过代理模式和反射机制来实现的。本文将详细介绍SpringAOP的技术实现原理,包括JDK动态代理和CGLIB代理的使用,并通过实例演示其在实际项目中的应用。1.AOP的实现原理概述SpringAOP的实现基于代理模式,通过代理对象来包......
  • Spring Boot入坑-AOP、Interceptor和Filter
    AOP概述面向切面编程(AspectOrientedProgramming),从不同的维度或角度给已有程序添加附加功能的一种技术实现的方式是Spring容器为程序创建代理,让调用方无感知的调用指定方法,在运行期间动态的“织入”其他逻辑主要目的是为了解耦弥补OOP中只能继承类或实现接口进行功......
  • RedisLock的AOP实现
    AOP的使用:1.标注@Aspect,说明此类是切面类。可以先用@Pointcut定义一个表达式。然后在@Around中引用这个@Pointcut的方法。更常见的是直接在Around里面定义。@Around("@annotation(注解的位置)")@anootation:注解带有xxx的。@args:参数带有xxx的。@within/@target:标识生效的具体类......
  • 师爷,翻译翻译什么叫AOP
    张麻子:汤师爷,翻译翻译,什么叫AOP?汤师爷:这还用翻译。张麻子:我让你翻译给我听,什么叫AOP?汤师爷:不用翻译,切面编程就是AOP啊。黄四郎:难道你听不懂什么叫AOP?张麻子:我就想让你翻译翻译,什么叫AOP!汤师爷:AOP嘛。张麻子:翻译出来给我听,什么他妈的叫AOP!什么他妈的叫他妈的AOP!汤师爷:什么......
  • 使用ChatGPT解决在Spring AOP中@Pointcut中的execution如何指定Controller的所有方法
    背景使用ChatGPT解决工作中遇到的问题,https://xinghuo.xfyun.cn/desk切指定类在SpringAOP中,@Pointcut注解用于定义切点表达式,而execution属性用于指定切点表达式的具体匹配规则。要指定Controller的所有方法,可以使用以下方法:使用类名和方法名进行精确匹配。例如,如果要匹配名......