首先我来介绍一个限流的API,是来自google的guava,guava的用法如下:
guava的maven依赖
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>18.0version>
dependency>
这是一段SpringBoot的项目的controller层代码,这里有使用guava的限流API
@RestController
@RequestMapping("/currentLimiting")
public class CurrentLimitingController {
/**
* 每秒生成2.0个令牌
*/
private RateLimiter rateLimiter = RateLimiter.create(2.0);
@GetMapping("/getMessage")
public String getMessage() {
return rateLimiter.tryAcquire() ? "访问成功!" : "当前访问人数过多,请稍后尝试!";
}
}
RateLimiter就是限流的API,首先要创建该对象。
然后tryAcquire()会返回一个能否获取到令牌的boolean值。
测试一下,浏览器狂按F5刷新可以看到效果。
上面的API使用起来还是比较麻烦,比如多种场景(限流规则不一样)下看起来就不是很简洁。我非常赞同使用框架化的思维去简化使用,像现在的Spring Boot框架,都是采用注解化的形式,只需要在目标处打上注解,可以说使用起来非常方便,看起来也很简洁。
所以这篇文章最重要的是如何实现封装注解以及框架化的思想,封装限流注解只不过是个举例罢了。
对于封装限流注解,我们应先缕清实现思路:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestCurrentLimiting {
/**
* 每秒访问的次数
* 默认可以访问20次
* @return
*/
double token() default 20;
/**
* 被限流拦截返回客户端的消息
* @return
*/
String message() default "无法访问!";
}
如何判断方法上是否有加上该限流注解,我们可以使用Spring的Aop技术,具体来说是环绕通知,它的通知方式是这样的:
@Component
@Aspect
public class CurrentLimitAop {
@Around("@annotation(com.symc.util.annotation.RequestCurrentLimiting)") //拦截的注解
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//拦截代码(目标方法之前的代码)
Object proceed = joinPoint.proceed(); //执行目标方法
//目标方法之后的代码
return proceed;
}
}
Aop拦截的目标是定义好的注解。
个人对注解的理解是,一旦你给某个目标打上某注解(这个目标可以是方法、属性、类、注解等等)那么这个目标就会继承这个注解的所有特性。这样的话,当Aop拦截注解时,也就相当于拦截了所有带该注解的目标。
那为什么选用环绕通知呢?原因是非常灵活地控制目标方法是否执行,当拦截的时候,就不允许业务逻辑层的代码执行,而其它的通知方法都难以控制目标方法的执行。
下面就是实现限流的具体代码:
@Component
@Aspect
public class CurrentLimitAop {
private ConcurrentHashMap<String, RateLimiter> rateLimiterMap =
new ConcurrentHashMap<>();
@Around("@annotation(com.symc.util.annotation.RequestCurrentLimiting)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取拦截的方法名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//获取拦截方法上的RequestCurrentLimiting注解对象
RequestCurrentLimiting requestCurrentLimitingAnnotation =
methodSignature.getMethod().getAnnotation(RequestCurrentLimiting.class);
//限流API
RateLimiter rateLimiter = rateLimiterMap.get(methodSignature.toString());
//如果没有rateLimiter,则创建一个
if (rateLimiter==null) {
//获取该方法的token(限流值)
rateLimiter = RateLimiter.create(requestCurrentLimitingAnnotation.token());
rateLimiterMap.put(methodSignature.toString(),rateLimiter);
}
//判断是否限流,若限流就return
if (!rateLimiter.tryAcquire()) {
return requestCurrentLimitingAnnotation.message();
}
//程序执行到这里说明没有被限流拦截,执行目标方法
Object proceed = joinPoint.proceed();
return proceed;
}
}
我们可以先测试一下,我的测试用例:
@RestController
@RequestMapping("/currentLimiting")
public class CurrentLimitingController {
/**
* 每秒生成2.0个令牌
*/
private RateLimiter rateLimiter = RateLimiter.create(2.0);
@GetMapping("/insert")
@RequestCurrentLimiting(token = 2.0,message = "insert失败,请稍后尝试!")
public String insert(){
return "insert success";
}
@GetMapping("/delete")
@RequestCurrentLimiting(token = 0.5,message = "delete失败,请稍后尝试!")
public String delete(Integer id){
return "delete success";
}
}
用浏览器方法,然后一直刷新就能看到效果啦。
这个代码的实现原理和执行过程是这样的:
首先我们在加注解的时候设置了访问量以及响应消息,当然,如果你的响应消息是JSON格式,你可以在源码上改进一下,这个不难。
当执行方法时,检查到 @RequestCurrentLimiting立马被Aop拦截下来(本质拦截的该注解),然后通过代码中的API获取到目标方法的完整方法名,像这样public void com.symc.controller.CurrentLimitingController.delete(Integer id)
,你可以自己打印在控制台观察一下。
然后通过反射机制拿到该方法上面的RequestCurrentLimiting注解的对象,后面我们需要从这个对象获取token和message。
之后就用到了guava的限流API,在这之前将限流执行的对象存储起来,创建一个ConCurrentHashMap来存储,目的是希望它能批量地用于多种不同的场景,并且减少创建guavaAPI对象的消耗,存储的key就用上面的方法名,value用guavaAPI对象。
如果被拦截的话,说明被限流了,业务代码不必执行,直接将message返回。否则的话就放行。
package com.symc.util.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Author: Forward Seen
* @CreateTime: 2022/05/01 15:52
* @Description: 限流注解
* 直接将注解打在控制层的方法上,需要配合Spring框架的Controller注解使用
* 你在使用的时候可以设置token的值来自定义每秒限制的访问量
* 还可以设置message的值来自定义返回给前端的消息数据
*
* 该注解依赖Spring的Aop技术、Google的guava限流API技术
* 实现技术:Aop环绕通知 + guava + 反射 + 注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestCurrentLimiting {
/**
* 每秒访问的次数
* 默认可以访问20次
* @return
*/
double token() default 20;
/**
* 被限流拦截返回客户端的消息
* @return
*/
String message() default "无法访问!";
}
package com.symc.aop;
import com.google.common.util.concurrent.RateLimiter;
import com.symc.util.annotation.RequestCurrentLimiting;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author: Forward Seen
* @CreateTime: 2022/05/01 16:04
* @Description: 用于拦截 public @interface com.symc.util.annotation.RequestCurrentLimiting
*/
@Component
@Aspect
public class CurrentLimitAop {
private ConcurrentHashMap<String, RateLimiter> rateLimiterMap =
new ConcurrentHashMap<>();
@Around("@annotation(com.symc.util.annotation.RequestCurrentLimiting)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
RequestCurrentLimiting requestCurrentLimitingAnnotation =
methodSignature.getMethod().getAnnotation(RequestCurrentLimiting.class);
RateLimiter rateLimiter = rateLimiterMap.get(methodSignature.toString());
if (rateLimiter==null) {
rateLimiter = RateLimiter.create(requestCurrentLimitingAnnotation.token());
rateLimiterMap.put(methodSignature.toString(),rateLimiter);
}
if (!rateLimiter.tryAcquire()) {
return requestCurrentLimitingAnnotation.message();
}
Object proceed = joinPoint.proceed();
return proceed;
}
}