前言: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个线程请求接口,只有少许请求能通过,达到了限流的目的