springboot 自定义注解 ,实现接口限流(计数器限流)【强行喂饭版】

news2024/11/27 15:31:25

思路:通过AOP拦截注解标记的方法,在Redis中维护一个计数器来记录接口访问的频率,
并根据限流策略来判断是否允许继续处理请求。

另一篇:springboot 自定义注解 ,aop切面@Around; 为接口实现日志插入【强行喂饭版】

不多说,直接上代码:

一:创建限流类型

/**
 * 限流类型
 * 
 */

public enum LimitType
{
    /**
     * 默认策略全局限流
     */
    DEFAULT,

    /**
     * 根据请求者IP进行限流
     */
    IP
}



二:创建注解

import 你上面限流类型的路径.LimitType;

import java.lang.annotation.*;

/**
 * 限流注解
 * 
 */
// 注解的作用目标为方法
@Target(ElementType.METHOD) 

// 注解在运行时保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用
@Retention(RetentionPolicy.RUNTIME) 

// 指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值
@Documented 
public @interface LimiterToShareApi{

    /**
     * 限流key
     */
    public String key() default "";

    /**
     * 限流时间,单位秒
     */
    public int time() default 60;

    /**
     * 限流次数
     */
    public int count() default 100;

    /**
     * 限流类型,默认全局限流
     */
    public LimitType limitType() default LimitType.DEFAULT;
}



**三:编写业务异常类 **

/**
 * 业务异常
 * 
 */
public final class ServiceException extends RuntimeException
{
	// 序列化的版本号的属性
    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    private Integer code;

    /**
     * 错误提示
     */
    private String message;

    /**
     * 空构造方法,避免反序列化问题
     */
    public ServiceException(){
    }

	 /**
     * 异常信息
     */
    public ServiceException(String message){
        this.message = message;
    }
    
}


四:实现aop切面拦截,限流逻辑处理

import 你上面限流类型的路径.LimitType;
import 你上面业务异常的路径.ServiceException;
import 你上面限流注解的路径.LimiterToShareApi;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

 
// 声明这是一个切面类
@Aspect
// 表明该类是一个组件,将该类交给spring管理。
@Component
// 指定执行顺序,值越小,越先执行。限流策略一般最先执行。
@Order(1) 
public class LimiterToShareApiAspect {

	// 记录日志的Logger对象
    private static final Logger log = LoggerFactory.getLogger(LimiterToShareApiAspect.class);

	// 操作Redis的RedisTemplate对象
    private RedisTemplate<Object, Object> redisTemplate;

	//在Redis中执行Lua脚本的对象
    private RedisScript<Long> limitScript;

    @Autowired
    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Autowired
    public void setLimitScript(RedisScript<Long> limitScript) {
        this.limitScript = limitScript;
    }

	// 这个注解作用及普及 见文章后面解析
    @Before("@annotation(limiter)")
    public void doBefore(JoinPoint point, LimiterToShareApi limiter) throws Throwable {
        
        // 根据业务需求,看看是否去数据库查询对应的限流策略,还是直接使用注解传递的值
        // 这里演示为 获取注解的值
        int time = limiter.time();
        int count = limiter.count();

        String combineKey = getCombineKey(limiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (number == null || number.intValue() > count) {
                throw new ServiceException("限流策略:访问过于频繁,请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }
    

    /**
     * 获取用于限流的组合键,根据LimterToShareApi注解和JoinPoint对象来生成。
     *
     * @param rateLimiter LimiterToShareApi注解,用于获取限流配置信息。
     * @param point       JoinPoint对象,用于获取目标方法的信息。
     * @return 生成的用于限流的组合键字符串。
     */
    public String getCombineKey(LimiterToShareApi rateLimiter, JoinPoint point) {
        // 创建一个StringBuffer用于拼接组合键
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key() + "-");

        // 根据LimterToShareApi注解的limitType判断是否需要添加IP地址信息到组合键中【判断限流类型 是否根据ip进行限流】
        if (rateLimiter.limitType() == LimitType.IP) {
            // 如果需要添加IP地址信息,调用IpUtils.getIpAddr()方法获取当前请求的IP地址,并添加到组合键中
            stringBuffer.append(getClientIp()).append("-");
        }

        // 使用JoinPoint对象获取目标方法的信息
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();

        // 将目标方法所属类的名称和方法名称添加到组合键中
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());

        // 返回生成的用于限流的组合键字符串
        return stringBuffer.toString();
    }


	 /**
     * 获取调用方真实ip [本机调用则得到127.0.0.1]
     * 首先尝试从X-Forwarded-For请求头获取IP地址,如果没有找到或者为unknown,则尝试从X-Real-IP请求头获取IP地址,
     * 最后再使用request.getRemoteAddr()方法作为备用方案。注意,在多个代理服务器的情况下,
     * X-Forwarded-For请求头可能包含多个IP地址,我们取第一个IP地址作为真实客户端的IP地址。
     */
    public String getClientIp() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String ipAddress = request.getHeader("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("X-Real-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
        }
        // 多个代理服务器时,取第一个IP地址
        int index = ipAddress.indexOf(",");
        if (index != -1) {
            ipAddress = ipAddress.substring(0, index);
        }
        return ipAddress;
    }

}

五:哪里需要点哪里

@PostMapping("/接口api")
// 根据自己业务选择是否需要这些参数,如果是想从数据库读取,不填参数即可
// 这里意思为对key的限制为 全局 每60秒内2次请求,超过2次则限流
@LimiterToShareApi(key = "key",time = 60,count = 100,limitType = LimitType.DEFAULT)
public AjaxResult selectToUserId(参数){}


限流(代码)结果:
在这里插入图片描述在这里插入图片描述在这里插入图片描述


解析:

@Before("@annotation(limiter)")- 使用了@Before 来表示这是一个切面注解,用于定义在目标方法执行前执行的逻辑

- @annotation(limiter) 中的limiter是指参数名称,而不是注解名称。

- @annotation(limiter) 中的limiter参数类型为LimiterToShareApi,
表示你将拦截被@LimiterToShareApi注解标记的方法,并且可以通过这个参数来获取@LimiterToShareApi注解的信息。
如果你想拦截其他注解,只需将第二个参数的类型修改为对应的注解类型即可。

普及:

JoinPointSpring AOP中的一个接口,它代表了在程序执行过程中能够被拦截的连接点(Join Point)。
连接点指的是在应用程序中,特定的代码块,比如方法的调用、方法的执行、构造器的调用等。

JoinPointAOP中的作用是用于传递方法调用的信息,比如方法名、参数、所属的类等等。
当AOP拦截到一个连接点时,就可以通过JoinPoint对象来获取这些信息,并根据需要进行相应的处理。

在AOP中,常见的通知类型(advice)如下:

@Before:在目标方法执行之前执行。
@After:在目标方法执行之后(无论是否抛出异常)执行。
@AfterReturning:在目标方法成功执行之后执行。
@AfterThrowing:在目标方法抛出异常后执行。
@Around:在目标方法执行前后都执行,可以控制目标方法的执行。

在以上各种通知中,可以使用JoinPoint参数来获取连接点的相关信息。
例如,在@Around通知中,可以使用JoinPoint对象来获取目标方法的信息,
比如方法名、参数等。这样,我们就可以根据这些信息来实现我们需要的切面逻辑。


eg:
// 获取方法名
String methodName = joinPoint.getSignature().getName();

//获取方法参数
Object[] args = joinPoint.getArgs();

// 获取所属类名
String className = joinPoint.getSignature().getDeclaringTypeName();

// 获取源代码位置信息
SourceLocation sourceLocation = joinPoint.getSourceLocation();

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode第 N 个泰波那契数 (认识动态规划)

认识动态规划 编写代码代码空间优化 链接: 第 N 个泰波那契数 编写代码 class Solution { public:int tribonacci(int n) {if(n 0){return 0;}else{if(n 1 || n 2)return 1;}vector<int> dp(n 1);dp[0] 0;dp[1] 1;dp[2] 1;for(int i 3;i < n;i){dp[i] dp[i-3]…

多巴胺色彩青春来袭!体验全能轻薄本华硕无畏Pro15 2023

这个烈日炎炎的夏季&#xff0c;还是来临了&#xff01; 40℃的高温&#xff0c;恍然才知道这个地球真的是在“发光发热”&#xff0c;对于莘莘学子来说&#xff0c;青春的炙热与憧憬也在即将告别的毕业季中变得热烈。回首停留在校园中的一幕幕&#xff0c;总有一些记忆&#…

javascript 7种继承-- new操作符的原理以及实现

new 运算符 new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 MDN参考资料&#xff1a;点击 语法 new constructor[([arguments])]参数 constructor 一个指定对象实例的类型的类或函数。 arguments 一个用于被 constructor 调用的参数列表。 …

JAVA基础-基于多线程的聊天程序

引言 什么是程序 &#xff1f; 一个程序可以有多个进程 。程序是一段静态的代码&#xff0c;它是应用程序执行的蓝本。 什么是进程 &#xff1f; 一个进程可以有多线程 进程是指一种正在运行的程序&#xff0c;有自己的地址空间。 作为蓝本的程序可以被多次加载到系统的不同内…

Visual Studio Code配置免密远程开发环境

VSCode安装插件 要是想连接远程服务器&#xff0c;先在本地安装下面的插件&#xff08;红色圈起来的需要装&#xff09; 连接远程服务器 配置服务器信息 保存然后再连接&#xff0c;输入密码&#xff0c;如果能连接上说明是没问题的&#xff0c;下面开始免密登录 免密配置 客…

【多线程带来的的风险-线程安全的问题的简单实例-线程不安全的原因】

文章目录 前言线程不安全的5大原因1. 抢占式执行和随机调度2. 多个线程同时修改一个变量(共享数据&#xff09;3. 修改操作不是原子性的4. 内存可见性5. 指令重排序 前言 什么是线程安全&#xff1f; 简单来说&#xff0c;如果多线程环境下代码运行的结果是符合我们预期的&am…

7月《中国数据库行业分析报告》已发布,聚焦图数据库、首发【全球图数据库产业图谱】

为了帮助大家及时了解中国数据库行业发展现状、梳理当前数据库市场环境和产品生态等情况&#xff0c;从2022年4月起&#xff0c;墨天轮社区行业分析研究团队出品将持续每月为大家推出最新《中国数据库行业分析报告》&#xff0c;持续传播数据技术知识、努力促进技术创新与行业生…

Yunfly 一款高效、性能优异的node.js企业级web框架

介绍 Yunfly 一款高性能 Node.js WEB 框架, 使用 Typescript 构建我们的应用。 使用 Koa2 做为 HTTP 底层框架, 使用 routing-controllers 、 typedi 来高效构建我们的 Node 应用。 Yunfly 在 Koa 框架之上提升了一个抽象级别, 但仍然支持 Koa 中间件。在此基础之上, 提供了一…

不同薪资阶段的Android 对“binder 的理解”

转载地址&#xff1a;https://zhuanlan.zhihu.com/p/420660511 面试官提了一个问题&#xff0c;我们来看看三位应聘者的表现如何吧 自认为无所不知&#xff0c;水平已达应用开发天花板&#xff0c;目前月薪 10k 面试官️&#xff1a;谈谈你对 binder 的理解 A&#xff1a;bind…

5-linux中的定时任务调度

定时任务调度 crond 任务调度概述基本语法常用选项快速入门应用实例crond 相关指令 at 定时任务基本介绍at 命令格式at 命令选项at 时间的定义其他指令 crond 任务调度 crontab 进行 定时任务调度 概述 任务调度&#xff1a;是指系统在某个时间执行的特定的命令或程序 任务…

CISP-PTE和CISP-PTS哪个更适合?

CISP-PTE和CISP-PTS是两个与网络安全渗透测试相关的认证&#xff0c;虽然它们有相似之处&#xff0c;但也存在一些区别。 CISP-PTE&#xff1a;证书持有人员主要从事信息安全技术领域网站渗透测试工作&#xff0c;具有规划测试方案、编写项目测试计划、编写测试用例、测试报告…

【C++ 异步任务 】`std::future`

1.基础知识 std::future 是 C 标准库中的一个类模板&#xff0c;定义在 <future> 头文件中。它提供了一种异步操作的机制&#xff0c;用于获取异步任务的结果。 std::future 类模板表示一个未来可能会获得的值。你可以将一个异步任务交给 std::future 来管理&#xff0…

苍穹外卖-day08 java实现 微信支付

苍穹外卖-day08 课程内容 导入地址簿功能代码用户下单订单支付 功能实现&#xff1a;用户下单、订单支付 用户下单效果图&#xff1a; 订单支付效果图&#xff1a; 1. 导入地址簿功能代码 1.1 需求分析和设计 1.1.1 产品原型 地址簿&#xff0c;指的是消费者用户的地址信息&…

Django学习笔记-视图(views)的使用

Django中可以使用views进行管理&#xff0c;类似于WPF的MVVM的ViewModel层&#xff0c;也相当于MVC架构的模Controller层。 一、基于函数的视图FBV&#xff08;Function-Based View&#xff09; 通过定义一个函数&#xff0c;包含HttpRequest对象作为参数&#xff0c;用来接受…

运维高级--shell脚本完成分库分表

为什么要进行分库分表 随着系统的运行&#xff0c;存储的数据量会越来越大&#xff0c;系统的访问的压力也会随之增大&#xff0c;如果一个库中的表数据超过了一定的数量&#xff0c;比如说MySQL中的表数据达到千万级别&#xff0c;就需要考虑进行分库分表&#xff1b; 其…

分享几个电脑录制GIF图片的小工具(附下载链接)

GIF工具 1.GIF1232.ScreentoGif3.gifcam4.LICECAP5.recordit 1.GIF123 点击下载: GIF123 介绍 GIF123&#xff1a;是单文件的绿色软件&#xff0c;无任何外部依赖项&#xff0c;体积仅755KB&#xff0c;支持windows所有版本操作简单&#xff01;&#xff01; 2.ScreentoGif …

梯度提升树的参数

目录 1. 迭代过程 1.1 初始预测结果的设置 1.2 使用回归器完成分类任务 ① 二分类情况 ② 多分类情况 1.3 GBDT的8种损失函数 ① 分类器中的loss a. 二分类交叉熵损失 b. 多分类交叉熵损失 c. 二分类指数损失 d. 多分类指数损失 ② 回归树中的loss a. 平方误差 b…

JPA连接达梦数据库导致auto-ddl失效问题解决

现象&#xff1a; 项目使用了JPA&#xff0c;并且auto-ddl设置的为update&#xff0c;在连接达梦数据库的时候&#xff0c;第一次启动没有问题&#xff0c;但是后面重启就会报错&#xff0c;发现错误为重复建表&#xff0c;也就是说已经建好的表没有检测到&#xff0c;…

spring 的循环依赖以及spring为什么要用三级缓存解决循环依赖

知识铺垫 bean的生命周期 这里简单过一下 class ->无参构造 ->普通对象 ->依赖注入&#xff08;对加了autowire等的属性赋值&#xff09; ->初始化前->初始化 ->初始化后&#xff08;aop&#xff09; ->放入单例池的map&#xff08;一级缓存&#xff09;…

第二次作业 运维高级 MySQL主从复制

1.理解MySQL主从复制原理 1、master&#xff08;binlog dump thread&#xff09;主要负责Master库中有数据更新的时候&#xff0c;会按照binlog格式&#xff0c;将更新的事件类型写入到主库的binlog文件中。 2、I/O thread线程在Slave中创建&#xff0c;该线程用于请求Master&…