一个注解实现频率控制

1.概述

抹茶项目是一个即时的IM通信项目,并且有着万人大群。但凡有几个人刷屏,那消息爆炸的场景,都不敢想象。如果我们需要对项目特定的接口进行频率控制,不仅是业务上的功能,同样也保护了项目的监控运行。而频控又是个很通用东西,好多地方都要用到,因此可以把它实现为一个小组件,也就是注解的形式使用。

2.效果展示

直接看效果,通过频控注解,很轻松的就实现接口的请求频率控制,防止有人瞎点。

有些接口还需要配置多种频控策略,这种我们可以再加个注解,将多个策略包起来。甚至通过一些配置,还能更简洁。

接下来就看看我们是怎么实现切面逻辑的吧。

3.注解实现

定义一个多策略的容器注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControlContainer {FrequencyControl[] value();
}

定义关键频控策略注解@FrequencyControl

关键就在于@Repeatable可重复的配置,这样就可以把相同注解加在一个方法上,猜测这是一个语法糖。

其中频控对象对应的就是 redis 中的一个 key,所以也需要 prefixKey 参数和 el 表达式 spEl 参数。time 和 unit 控制统计的时间范围,count 是次数。提供target是因为我们的频控大多是用在接口上的,并且接口拦截器会解析出用户的 ip 和 uid。而很多的场景是直接对 uid 或者 ip 做频率控制的。针对这种情况,我们指定了 uid 后,连 el 表达式都可以不用写了,切面会自动从上下文中获取 uid,让注解的实现更加简洁。

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;/*** 频控注解*/
@Repeatable(FrequencyControlContainer.class)//可重复
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControl {/*** key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定** @return key的前缀*/String prefixKey() default "";/*** 频控对象,默认el表达指定具体的频控对象* 对于ip 和uid模式,需要是http入口的对象,保证RequestHolder里有值** @return 对象*/Target target() default Target.EL;/*** springEl 表达式,target=EL必填** @return 表达式*/String spEl() default "";/*** 频控时间范围,默认单位秒** @return 时间范围*/int time();/*** 频控时间单位,默认秒** @return 单位*/TimeUnit unit() default TimeUnit.SECONDS;/*** 单位时间内最大访问次数** @return 次数*/int count();enum Target {UID, IP, EL}
}

4.切面

根据不同的频控对象,组装不同的key。前缀默认也是类名+方法名。由于有多个相同注解,我们还需要给每个频控对象加上一个专属下标,防止重复,所以新增的频控策略注解要加在最下方。

redis实现频控其实有三种选择,固定时间,滑动窗口,令牌桶。我们选择的是最简单的固定时间的方式,在指定时间统计次数,超过就限流。通过expire来实现指定时间,以及过期重置的效果。

思路可以拓展一下,后续增加不同的底层实现策略,并且在注解开个参数开放配置不同的策略

import cn.hutool.core.util.StrUtil;
import com.abin.mallchat.common.common.annotation.FrequencyControl;
import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
import com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlUtil;
import com.abin.mallchat.common.common.utils.RequestHolder;
import com.abin.mallchat.common.common.utils.SpElUtils;
import lombok.extern.slf4j.Slf4j;
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.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;import static com.abin.mallchat.common.common.service.frequencycontrol.FrequencyControlStrategyFactory.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;/*** Description: 频控实现*/
@Slf4j
@Aspect
@Component
public class FrequencyControlAspect {@Around("@annotation(com.abin.mallchat.common.common.annotation.FrequencyControl)||@annotation(com.abin.mallchat.common.common.annotation.FrequencyControlContainer)")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class);Map<String, FrequencyControl> keyMap = new HashMap<>();for (int i = 0; i < annotationsByType.length; i++) {FrequencyControl frequencyControl = annotationsByType[i];String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? SpElUtils.getMethodKey(method) + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)String key = "";switch (frequencyControl.target()) {case EL:key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());break;case IP:key = RequestHolder.get().getIp();break;case UID:key = RequestHolder.get().getUid().toString();}keyMap.put(prefix + ":" + key, frequencyControl);}// 将注解的参数转换为编程式调用需要的参数List<FrequencyControlDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildFrequencyControlDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());// 调用编程式注解return FrequencyControlUtil.executeWithFrequencyControlList(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTOS, joinPoint::proceed);}/*** 将注解参数转换为编程式调用所需要的参数** @param key              频率控制Key* @param frequencyControl 注解* @return 编程式调用所需要的参数-FrequencyControlDTO*/private FrequencyControlDTO buildFrequencyControlDTO(String key, FrequencyControl frequencyControl) {FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();frequencyControlDTO.setCount(frequencyControl.count());frequencyControlDTO.setTime(frequencyControl.time());frequencyControlDTO.setUnit(frequencyControl.unit());frequencyControlDTO.setKey(key);return frequencyControlDTO;}
}

限流工具类

import com.abin.mallchat.common.common.domain.dto.FrequencyControlDTO;
import com.abin.mallchat.common.common.utils.AssertUtil;
import org.apache.commons.lang3.ObjectUtils;import java.util.List;/*** 限流工具类 提供编程式的限流调用方法*/
public class FrequencyControlUtil {/*** 单限流策略的调用方法-编程式调用** @param strategyName     策略名称* @param frequencyControl 单个频控对象* @param supplier         服务提供着* @return 业务方法执行结果* @throws Throwable*/public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);return frequencyController.executeWithFrequencyControl(frequencyControl, supplier);}public static <K extends FrequencyControlDTO> void executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.Executor executor) throws Throwable {AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);frequencyController.executeWithFrequencyControl(frequencyControl, () -> {executor.execute();return null;});}/*** 多限流策略的编程式调用方法调用方法** @param strategyName         策略名称* @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序* @param supplier             函数式入参-代表每个频控方法执行的不同的业务逻辑* @return 业务方法执行的返回值* @throws Throwable 被限流或者限流策略定义错误*/public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControlList(String strategyName, List<K> frequencyControlList, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier);}/*** 构造器私有*/private FrequencyControlUtil() {}
}

5.SPEL表达式 

SpEL(Spring Expression Language),即Spring表达式语言,能在运行时构建复杂表达式、存取对象属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。

使用场景:在spring cache中就经常使用了

@Override
@Cacheable(value = "rbac:roleSet", key = "T(org.apache.commons.lang3.StringUtils).join(#roles,'|')", unless = "#result == null || #result.size() == 0")
public List<String> getRoleIdsByRole(Set<String> roles) {return null;
}

实现原理

  1. 创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现

  2. 解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象

  3. 构造上下文:准备比如变量定义等等表达式需要的上下文数据。

  4. 求值:通过Expression接口的getValue方法根据上下文(EvaluationContext,RootObject)获得表达式值。

最小例子:一个最简单的使用el表达式的例子

public static void main(String[] args) {List<Integer> primes = new ArrayList<Integer>();primes.addAll(Arrays.asList(2,3,5,7,11,13,17));// 创建解析器ExpressionParser parser = new SpelExpressionParser();//构造上下文StandardEvaluationContext context = new StandardEvaluationContext();context.setVariable("primes",primes);//解析表达式Expression exp =parser.parseExpression("#primes.?[#this>10]");// 求值List<Integer> primesGreaterThanTen = (List<Integer>)exp.getValue(context);
}

思考下,为啥我们能通过 el 表达式拿到方法入参的值

那肯定是spring把入参全部放入上下文中了,对吧!

还有一点,我们jdk反射拿到的参数,是没有参数名的,都是arg0,arg1。真想拿到参数名,还有一点儿难度。所以我们还要借助spring的参数解析器DefaultParameterNameDiscoverer,具体的原理可以看:论java如何通过反射获得方法真实参数名及扩展研究_java_AB教程网。

SPEL工具类:由于我们的两个注解都需要el解析,也都需要类名+方法名作为前缀,于是我把通用的逻辑抽成了一个工具类。

public class SpElUtils {private static final ExpressionParser parser = new SpelExpressionParser();private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();public static String parseSpEl(Method method, Object[] args, String spEl) {String[] params = parameterNameDiscoverer.getParameterNames(method);//解析参数名EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象for (int i = 0; i < params.length; i++) {context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去}Expression expression = parser.parseExpression(spEl);return expression.getValue(context, String.class);}public static String getMethodKey(Method method){return method.getDeclaringClass()+"#"+method.getName();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/265681.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端架构: 脚手架之多package项目管理和架构

多package项目管理 1 &#xff09;多package项目管理概述 通常来说&#xff0c;当一个项目变大了以后&#xff0c;我们就要对这个项目进行拆分在前端当中&#xff0c;对于项目进行拆分的方式&#xff0c;通常把它称之为javascript包管理需要使用一个工具叫做 npm (Node Packag…

我的Java美团求职之路,2022非科班生的Java面试之路

目录 进入Spring Boot世界 讲述Sping、Spring Boot 和Spring Cloud 之间的关系&#xff0c;还重点讲述了如何利用开发工具(如IDEA)来实现开发&#xff0c;如何通过API文档来寻找类对象方法&#xff0c;告诉我们在开发过程中如何学习、发现和解决问题 需要免费领取这份Alibaba…

5G双域快网

目录 一、业务场景 二、三类技术方案 2.1、专用DNN方案 2.2、ULCL方案&#xff1a;通用/专用DNNULCL分流 2.3、 多DNN方案-定制终端无感分流方案 漫游场景 一、业务场景 初期双域专网业务可划分为三类业务场景&#xff0c;学校、政务、文旅等行业均已提出公/专网融合访问需…

跳跃游戏Ⅱ

问题 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - …

Stable-Diffusion ubuntu服务器部署,报错解决方法(小白教程)

Stable Diffusion是一个深度学习模型&#xff0c;专注于生成高质量的图像。它由CompVis团队与Stability AI合作开发&#xff0c;并在2022年公开发布。这个模型使用文本提示&#xff08;text prompts&#xff09;生成详细、逼真的图像&#xff0c;是目前人工智能图像生成领域的一…

金融行业专题|期货超融合架构转型与场景探索合集(2023版)

更新内容&#xff1a; 更新 SmartX 超融合在期货行业的覆盖范围、部署规模与应用场景。新增 CTP 主席系统实践与评测、容器云资源池等场景实践。更多超融合金融核心生产业务场景实践&#xff0c;欢迎下载阅读电子书《SmartX 金融核心生产业务场景探索文章合集》。 面对不断变…

CI/CD笔记.Gitlab系列.`gitlab-ci.yml`中的头部关键字

CI/CD笔记.Gitlab系列 gitlab-ci.yml中的头部关键字 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at: https://jclee95.blog.csdn.netEmail: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28550263/article/details/136342897HuaW…

cRIO9040中NI9871模块的测试

硬件准备 CompactRIO9040NI9871直流电源&#xff08;可调&#xff09;网线RJ50转DB9线鸣志STF03-R驱动器和步进电机 软件安装 参考&#xff1a;cRIO9040中NI9381模块的测试 此外&#xff0c;需安装NI-Serial 9870和9871扫描引擎支持 打开NI Measurement&#xff06;Automa…

基于Java SSM springboot+VUE+redis实现的前后端分类版网上商城项目

基于Java SSM springbootVUEredis实现的前后端分类版网上商城项目 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐…

fastjson序列化MessageExt对象问题(1.2.78之前版本)

前言 无论是kafka&#xff0c;还是RocketMq&#xff0c;消费者方法参数中的MessageExt对象不能被 fastjson默认的方式序列化。 一、查看代码 Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,ConsumeConcurrentlyContext context) {t…

【MATLAB】SVMD_ MFE_SVM_LSTM 神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 SVMD_MFE_SVM_LSTM神经网络时序预测算法结合了单变量分解&#xff08;SVMD&#xff09;、多尺度特征提取&#xff08;MFE&#xff09;、聚类后展开支持向量机&#xff08;SVM&#xff09;…

【Ansys Fluent Web 】全新用户界面支持访问大规模多GPU CFD仿真

基于Web的技术将释放云计算的强大功能&#xff0c;加速CFD仿真&#xff0c;从而减少对硬件资源的依赖。 主要亮点 ✔ 使用Ansys Fluent Web用户界面™&#xff08;UI&#xff09;&#xff0c;用户可通过任何设备与云端运行的仿真进行远程交互 ✔ 该界面通过利用多GPU和云计算功…

MIT-BEVFusion系列九--CUDA-BEVFusion部署4 c++解析pytorch导出的tensor数据

目录 创建流打印 engine 信息打印结果内部流程 启动计时功能加载变换矩阵并更新数据&#xff08;重要&#xff09;内部实现 该系列文章与qwe、Dorothea一同创作&#xff0c;喜欢的话不妨点个赞。 在create_core方法结束后&#xff0c;我们的视角回到了main.cpp中。继续来看接下…

[vscode] 1. 在编辑器的标签页下显示文件目录(标签页显示面包屑) 2. 在标题栏上显示当前文件的完整路径

1. 标签页显示面包屑 view->Appearance->Breadcrumbs 2. 在标题栏上显示当前文件的完整路径 搜索 window.title将原来的值activeEditorShort 修改为 activeEditorMedium 参考&#xff1a; vscode在编辑器的标签页下显示文件目录&#xff08;标签页显示面包屑&#xf…

10、电源管理入门之OPP介绍

目录 1. 什么是OPP,怎么用? 2. 系统初始化加载OPP信息 3. 触发使用 4. API介绍 之前的文章设置clock的时候多次提到了(Operating Performance Point)OPP,例如DEVFreq、CPUFreq等,在现代SoC上存在有Power Domain,也可以以Power Domain为单位进行OPP的电压频率定义。 …

C++ 游戏飞机大战, 字符型的

//#define _CRT_SECURE_NO_WARNINGS 1 用于禁止不安全函数的警告 #include<iostream> #include<stdlib.h> #include<string> #include<conio.h> #include<Windows.h> #include<time.h> #include <graphics.h> using namespace std;…

顶会ICLR2024论文Time-LLM:基于大语言模型的时间序列预测

文青松 松鼠AI首席科学家、AI研究院负责人 美国佐治亚理工学院(Georgia Tech)电子与计算机工程博士&#xff0c;人工智能、决策智能和信号处理方向专家&#xff0c;在松鼠AI、阿里、Marvell等公司超10年的技术和管理经验&#xff0c;近100篇文章发表在人工智能相关的顶会与顶刊…

SpringMVC 学习(六)之视图

目录 1 SpringMVC 视图介绍 2 JSP 视图 3 Thymeleaf 视图 4 FreeMarker 视图 5 XSLT 视图 6 请求转发与重定向 6.1 请求转发 (Forward) 6.2 重定向 (Redirect) 7 视图控制器 (view-controller) 1 SpringMVC 视图介绍 在 SpringMVC 框架中&#xff0c;视图可以是一个 J…

字节面试问题

实现三列布局的方法 第一种&#xff1a;可以使用浮动margin 第二种&#xff1a;浮动BFC <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, in…

防御保护:防火墙内容安全

一、IAE&#xff08;Intelligent Awareness Engine&#xff09;引擎 二、深度检测技术(DFI和DPI&#xff09; 1.DPI – 深度包检测技术 DPI主要针对完整的数据包&#xff08;数据包分片&#xff0c;分段需要重组&#xff09;&#xff0c;之后对数据包的内容进行识别。&#x…