主要作用是根据时间限制前台向后台的请求,通过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和操作来限制某时间内访问的次数

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

被折叠的 条评论
为什么被折叠?



