一个注解实现频率控制

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.mfbz.cn/a/415052.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

幻兽帕鲁(1.5.0)可视化管理工具(0.5.7 docker版)安装教程

文章目录 局域网帕鲁服务器部署教程帕鲁服务可视化工具安装配置服务器地址&#xff08;可跳过&#xff09;使用工具管理面板 1.5.0服务端RCON错误1.5.0服务端无法启动RCON端口 解决方法第一步&#xff1a;PalWorldSettings.ini配置第二步&#xff1a;修改PalServer.sh配置 局域…

数据结构--二叉排序树(Binary Search Tree,简称BST)

这里写自定义目录标题 二叉排序树二叉排序树与排序数组没有排序数组&#xff0c;链式存储链表的对比二叉排序树概念对于搜索操作&#xff0c;对于插入操作&#xff0c;对于删除操作&#xff0c; 分析删除节点代码运行结果 二叉排序树 二叉排序树与排序数组没有排序数组&#x…

python自动化管理和zabbix监控网络设备(防火墙和python自动化配置部分)

目录 前言 一、ssh配置 1.FW1 2.core-sw1 3.core-sw2 二、python自动化配置防火墙 三、验证DNAT 四、验证DNAT 前言 视频演示请访问b站主页 白帽小丑的个人空间-白帽小丑个人主页-哔哩哔哩视频 一、ssh配置 给需要自动化管理的设备配置ssh服务端用户名和密码 1.FW1 …

Linux NFC 子系统剖析

1.总览 linux源码中NFC在net/nfc下&#xff0c;文件结构如下图&#xff1a; hci&#xff1a;Host Controller Interface 主要是针对NFC的主机-控制器接口协议 nci&#xff1a;NFC Controller Interface 主要是NFC的控制器接口协议&#xff0c;用于NFCC(NFC Controller)和DH(…

进程的控制

文章目录 进程退出进程等待进程程序替换 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。 点击跳转到网站。 进程退出 进程的退出一共有三种场景。 程序跑完…

nacos开启鉴权+springboot配置用户名密码

nacos默认没有开启鉴权&#xff0c;springboot无需用户名密码即可连接nacos。从2.2.2版本开始&#xff0c;默认控制台也无需登录直接可进行操作。 因此本文记录一下如何开启鉴权&#xff0c;基于nacos2.3.0版本。 编辑nacos服务端的application.properties&#xff1a; # 开…

硬件工程师入门基础知识(二)片式电阻、片式网络电阻标识和焊接使用注意事项

片式电阻、片式网络电阻标识和使用注意事项 1.概述2.阻值标识2.1 标识原则2.2片式类电阻、片式电阻网络标识方法2.3电阻网络标识方法 3.电阻产品包装3.1片式电阻、片式电阻网络的包装3.2电阻网络的包装 4.使用注意事项4.1 片式电阻推荐焊盘尺寸4.2片式电阻网络推荐焊盘尺寸 5.焊…

STM32 +合宙1.54“ 电子墨水屏(e-paper)驱动显示示例

STM32 合宙1.54“ 电子墨水屏&#xff08;e-paper&#xff09;驱动显示示例 &#x1f4cd;相关篇《Arduino框架下ESP32/ESP8266合宙1.54“ 电子墨水屏&#xff08;e-paper&#xff09;驱动显示示例》&#x1f516;程序是从GooDisplay品牌和微雪电子下同型号规格墨水屏的示例程序…

Vue项目 快速上手(如何新建Vue项目,启动Vue项目,Vue的生命周期,Vue的常用指令)

目录 一.什么Vue框架 二.如何新建一个Vue项目 1.使用命令行新建Vue项目 2.使用图形化界面新建Vue项目 三.Vue项目的启动 启动Vue项目 1.通过VScode提供的图形化界面启动Vue项目 2.通过命令行的方式启动Vue项目 四.Vue项目的基础使用 常用指令 v-bind 和 v-model v…

【Unity每日一记】角色控制器Character Contorller

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

c# .net8 香橙派orangepi + hc-04蓝牙 实例

这些使用c# .net8开发,硬件 香橙派 orangepi 3lts和 hc-04蓝牙 使用场景:可以通过这个功能,手机连接orangepi进行wifi等参数配置 硬件: 1、带USB口的linux开发板orangepi 2、USB 转TTL 中转接蓝牙(HC-04) 某宝上买的蓝牙官方网有调试工具:HC-T串口助手 https://www…

leetcode 3.反转链表;

1.题目&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 2.用例&#xff1a; 3.题目解析&#xff1a; &#xff08;1&#xff09;函数头&#xff1a; 要求返回结点&#xff0c;就 ListNode* reverseList(ListNode* head)&…

【数据开发】大数据岗位,通用必备技术栈(数据分析、数据工程、数据科学)

【数据开发】大数据岗位&#xff0c;通用必备技术栈&#xff08;数据分析、数据工程、数据科学&#xff09; 文章目录 1、岗位与技术要求1.1 常见岗位介绍1.2 行业发展方向1.3 附部分JD 2、数据开发技术栈2.1 数据处理流程2.2 学习路线与框架 3、数据分析技术栈3.1 基础知识3.2…

如何一步一步地优化LVGL的丝滑度

经过一番周折将LVGL移植到了STM32F407单片机上&#xff0c;底层驱动的LCD是st7789&#xff0c;移植时的条件和环境如下&#xff1a; ●LVGL用的是单缓冲&#xff0c;一次刷新10行&#xff1b; ●刷新函数用的是最原始的一个一个打点的方式&#xff1b; ●ST7789底层发送数据用的…

【MySQL】学习和总结标量子查询

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-kLo6jykc7AcEVEQk {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

JMeter--9.录制脚本

录制步骤 1.新建线程组&#xff1a;测试计划->线程->线程组 测试计划下&#xff0c;至少要有1个线程组&#xff0c;因为在录制器中需要选择【目标控制器】 2. 新建录制器&#xff1a;测试计划->非测试原件->HTTP(S)测试脚本记录器&#xff08;HTTP代理服务器&…

Linux磁盘如何分区?

首先需要先给虚拟机添加磁盘 sblk #查看磁盘设备 得到以下内容&#xff1a; NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT sda 8:0 0 20G 0 disk ├─sda1 8:1 0 1G 0 part /boot └─sda2 8:2 0 19G 0 pa…

毕业后的那两年,我是怎么从一个啥也不会的小白成长为成熟职场人的?

对于2023应届生而言&#xff0c;从毕业到踏入职场也许正是你人生中很大的一个变化&#xff0c;但在初入职场的期间&#xff0c;很多同学很容易因为一些经验问题而误入弯路。 笔者从一个职场萌新到如今的职场老人&#xff0c;一路走来也经历了不少社会毒打。在职场生涯中&#…

kubectl 命令行管理K8S(上)

目录 陈述式资源管理方式 介绍 命令 项目的生命周期 创建 kubectl create命令 发布 kubectl expose命令 更新 kubectl set 回滚 kubectl rollout 删除 kubectl delete 应用发布策略 金丝雀发布 陈述式资源管理方式 介绍 1.kubernetes 集群管理集群资源…

Nest.js权限管理系统开发(八)jwt登录

安装相关依赖 虽然仅使用nestjs/jwt就能实现身份验证的功能&#xff0c;但是使用passport能在更高层次上提供更多便利。Passport 拥有丰富的 strategies 生态系统&#xff0c;实现了各种身份验证机制。虽然概念简单&#xff0c;但你可以选择的 Passport 策略集非常丰富且种类繁…
最新文章