自定义注解,并编写一个aop,通过使用redis实现前台请求限流

本文介绍了一种基于注解和AOP的限流机制,通过自定义注解MyXL结合AspectJ实现对HTTP请求的频率控制,详细解析了环绕通知与前置通知的区别,以及如何利用Redis进行IP限流。

主要作用是根据时间限制前台向后台的请求,通过aop切面实现方法之前检测请求次数

package cn.shangze.boot.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 限流注解(方法上的注解很重要,一定要注意)
 * @author leontius
 */
@Target(ElementType.METHOD)//作用于方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyXL {

    /**
     * @Description: 允许访问1次
     * @author: Hanweihu
     * @date: 2019/8/6 11:01
     */
    int limit() default 1;

    /**
     * @Description: 每1秒
     * @author: Hanweihu
     * @date: 2019/8/6 11:01
     */
    int timeout() default 1000;
}

调用的的方法为:直接在方法上引入就可以@MyXL  示例意思为注解方法里面给的一秒请求一次  或  @MyXL(limit = 1,timeout = 10000)  示例的意思为:10秒请求一次

aop切面拦截调用上面的注解,实现ip限流(这里注意一下,下面有切面的两种使用,第一种采用@Around() (在别的地方看到,该方法通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了),该方法才可以使用Proceedingjoinpoint,环绕通知 ProceedingJoinPoint 执行proceed方法的作用是让目标方法执行,这也是环绕通知和前置、后置通知方法的一个最大区别。Proceedingjoinpoint 继承了 JoinPoint 。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。

 第二种采用  @Before() 在方法之前执行,该方法不能使用 ProceedingJoinPoint ,只能采用JoinPoint ,我个人的理解就是ProceedingJoinPoint继承了JoinPoint,并且多了自己的方法,想当与JoinPoint的升级版,采用@Before()方法的话就不能有出参,如果引用出参就起不到限制作用,那么需要限流的接口就实现不了限流的需求了,采用void只能抛出异常来通知前台,这样能满足功能,但是会在log日志里面打印报错信息

 关于JointPoint和ProceedingJoinPoint使用详解 可以观看这两个帖子

https://blog.youkuaiyun.com/kouryoushine/article/details/105299956

https://blog.youkuaiyun.com/wuzhiwei549/article/details/79789853

package cn.shangze.boot.common.aop;

import cn.hutool.core.util.StrUtil;
import cn.shangze.boot.common.annotation.MyXL;
import cn.shangze.boot.common.limit.RedisRaterLimiter;
import cn.shangze.boot.common.utils.ResultUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Aspect
@Component
public class MyXLAspect {
    private final static Logger logger = LoggerFactory.getLogger(MyXLAspect.class);

    @Autowired(required = false)
    private HttpServletRequest request;
    @Autowired
    private RedisRaterLimiter redisRaterLimiter;

    /**
     * Controller层切点,注解方式
     */
    @Pointcut("@annotation(cn.shangze.boot.common.annotation.MyXL)")
    public void controllerAspect() {
    }

    /**
     * @Description: 注解限流(与下面@Before取其一)
     * @date: 2019/8/6 16:06
     * @params: [pjp]
     * @return: java.lang.Object
     */
    @ResponseBody
    @Around(value = "controllerAspect()")
    public Object aroundNotice(ProceedingJoinPoint pjp) throws Throwable {
//        logger.info("拦截到了{}方法...", pjp.getSignature().getName());
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        //获取目标方法
        Method targetMethod = methodSignature.getMethod();
        if (targetMethod.isAnnotationPresent(MyXL.class)) {
            //获取目标方法的@LxRateLimit注解
            MyXL lxRateLimit = targetMethod.getAnnotation(MyXL.class);
            //生成一个key值,该值是要存放到redis中的
            String redisKey = request.getRequestURI()+"::" +"123456789123456798";
            //将上面生成的key值和注解中的时间、次数传入下面的方法,实现限流
            String token = redisRaterLimiter.acquireTokenFromBucket(redisKey, lxRateLimit.limit(), lxRateLimit.timeout());
            if (StrUtil.isBlank(token)) {
                return new ResultUtil<Object>().setErrorMsg((lxRateLimit.timeout()/1000) +"秒后再试");
            }
        }
        return pjp.proceed();
    }

    
    /**
     * @Description: 注解限流2 (与上面@Around 取其一)
     * @date: 2019/8/6 16:06
     * @params: [pjp]
     * @return: java.lang.Object
     */
    @Before("controllerAspect()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        //获取目标方法
        Method targetMethod = methodSignature.getMethod();
        if (targetMethod.isAnnotationPresent(MyXL.class)) {
            //获取目标方法的@LxRateLimit注解
            MyXL lxRateLimit = targetMethod.getAnnotation(MyXL.class);
            String redisKey = request.getRequestURI()+"::" + ipInfoUtil.getIpAddr(request);
            String token = redisRaterLimiter.acquireTokenFromBucket(redisKey, lxRateLimit.limit(), lxRateLimit.timeout());
            if (StrUtil.isBlank(token)) {
                throw new XbootException((lxRateLimit.timeout()/1000) +"秒后再试");
            }
        }
    }

}

redis实现限流的限制器方法

package cn.shangze.boot.common.limit;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.ZParams;

import java.util.List;
import java.util.UUID;

/**
 * @author wangiegie@gmail.com https://gitee.com/boding1/pig-cloud
 */
@Component
@Slf4j
public class RedisRaterLimiter {

    @Autowired
    private JedisPool jedisPool;

    private static final String BUCKET = "BUCKET:";
    private static final String BUCKET_COUNT = "BUCKET_COUNT:";
    private static final String BUCKET_MONITOR = "BUCKET_MONITOR:";

    public String acquireTokenFromBucket(String point, int limit, long timeout) {

        Jedis jedis = jedisPool.getResource();
        try{
            //UUID令牌
            String token = UUID.randomUUID().toString();
            long now = System.currentTimeMillis();
            //开启事务
            Transaction transaction = jedis.multi();

            //删除信号量 移除有序集中指定区间(score)内的所有成员 ZREMRANGEBYSCORE key min max
            transaction.zremrangeByScore((BUCKET_MONITOR + point).getBytes(), "-inf".getBytes(), String.valueOf(now - timeout).getBytes());
            //为每个有序集分别指定一个乘法因子(默认设置为 1) 每个成员的score值在传递给聚合函数之前都要先乘以该因子
            ZParams params = new ZParams();
            params.weightsByDouble(1.0, 0.0);
            //计算给定的一个或多个有序集的交集
            transaction.zinterstore(BUCKET + point, params, BUCKET + point, BUCKET_MONITOR + point);

            //计数器自增
            transaction.incr(BUCKET_COUNT);
            List<Object> results = transaction.exec();
            long counter = (Long) results.get(results.size() - 1);

            transaction = jedis.multi();
            //Zadd 将一个或多个成员元素及其分数值(score)加入到有序集当中
            transaction.zadd(BUCKET_MONITOR + point, now, token);
            transaction.zadd(BUCKET + point, counter, token);
            transaction.zrank(BUCKET + point, token);
            results = transaction.exec();
            //获取排名,判断请求是否取得了信号量
            long rank = (Long) results.get(results.size() - 1);
            if (rank < limit) {
                return token;
            } else {
                //没有获取到信号量,清理之前放入redis中垃圾数据
                transaction = jedis.multi();
                //Zrem移除有序集中的一个或多个成员
                transaction.zrem(BUCKET_MONITOR + point, token);
                transaction.zrem(BUCKET + point, token);
                transaction.exec();
            }
        }catch (Exception e){
            log.error("限流出错,请检查Redis运行状态\n"+e.toString());
        }finally {
            if(jedis!=null){
                jedis.close();
            }
        }
        return null;
    }
}

将以上内容整理好放到自己的项目里面就可以通过访问者ip和操作来限制某时间内访问的次数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值