【实战教程】使用Spring AOP和自定义注解监控接口调用
一、背景
随着项目的长期运行和迭代,积累的功能日益繁多,但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。
为了提高系统性能和代码质量,我们往往需要对那些不常用的功能进行下线处理。
那么,该下线哪些功能呢?
此时,我们就需要对接口的调用情况进行统计和分析了!
二、实战
以下内容为主要代码,完整代码请参考:https://gitee.com/regexpei/daily-learning-test
以下使用 自定义注解 + AOP 的方式,对接口调用进行记录。
1. 创建项目,添加依赖
<dependencies> <!-- 提供自动配置、日志、YAML等核心功能 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- 提供面向切面编程支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 用于构建Web,包括RESTful和基于Servlet的Web应用,包含了Spring MVC、Tomcat等 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 通过注解减少样板代码的Java库,自动生成getter、setter等方法 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Swagger的注解库,允许开发者为API添加文档和元数据 --> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.2</version> </dependency> <!-- 用于Java对象的JSON序列化/反序列化的库,Fastjson的继任者 --> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.41</version> </dependency> <!-- 为Spring Boot应用提供了测试所需的依赖项,包括JUnit等,但仅限于测试阶段 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <!-- 排除已包含的SLF4J API版本,避免版本冲突 --> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <!-- Java工具包,提供了许多实用的工具类和方法 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.25</version> </dependency>
</dependencies>
2. 自定义注解和实体类
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface ApiOprLogAnno {@ApiModelProperty(value = "接口类型")String apiType() default "";@ApiModelProperty(value = "接口说明")String apiDetail() default "";@ApiModelProperty(value = "是否保存请求参数")boolean isSaveRequest() default false;@ApiModelProperty(value = "是否保存响应结果")boolean isSaveResponse() default false;}
@Setter
@Getter
public class ApiOprLog {@ApiModelProperty(name = "主键")private String id;@ApiModelProperty(name = "源IP")private String sourceIp;@ApiModelProperty(name = "用户名")private String username;@ApiModelProperty(name = "方法")private String method;@ApiModelProperty(name = "请求参数")private String reqParams;@ApiModelProperty(name = "响应结果")private String resResult;@ApiModelProperty(name = "异常信息")private String exMessage;@ApiModelProperty(name = "异常详细")private String exJson;@ApiModelProperty(name = "接口模块")private String apiModule;@ApiModelProperty(name = "接口类型")private String apiType;@ApiModelProperty(name = "接口说明")private String apiDetail;@ApiModelProperty(name = "创建时间")private Date createTime;@ApiModelProperty(name = "更新时间")private Date updateTime;
}
3. 创建切面类
@Slf4j
@Aspect
@Component
public class ApiOprAspect {@Value("${spring.application.name}")private String moduleName;/*** 从请求中获取 IP** @return IP*/private static String getIpFromRequest() {RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);return IpUtil.getRealIp(request);}return Constants.UNKNOWN;}@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")public void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {String id = IdUtil.fastSimpleUUID();Object result;try {// 执行方法前操作executeBefore(proceedingJoinPoint, id);result = proceedingJoinPoint.proceed();// 执行方法后操作executeAfter(proceedingJoinPoint, id, result);} catch (Throwable ex) {// 执行方法异常后操作executeAfterEx(ex, id);throw ex;}return result;}private void executeBefore(ProceedingJoinPoint proceedingJoinPoint, String id) {// 获取目标方法的签名信息MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();// 从方法签名中获取 ApiOprLogAnno 注解的信息ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);// 封装 ApiOprLog 对象ApiOprLog apiOprLog = packaging(id, getIpFromRequest(), signature.toString(), apiOprLogAnno);if (apiOprLogAnno.isSaveRequest()) {// 保存请求参数// 获取方法签名的参数名数组String[] parameterNames = signature.getParameterNames();// 获取连接点传递的实参数组 Object[] args = proceedingJoinPoint.getArgs();Map<String, Object> paramMap = new HashMap<>(parameterNames.length);for (int i = 0; i < parameterNames.length; i++) {if (!RequestAttributes.REFERENCE_REQUEST.equals(parameterNames[i])) {paramMap.put(parameterNames[i], args[i]);}}apiOprLog.setReqParams(JSON.toJSONString(paramMap));}// 入库操作log.debug("executeBefore apiOprLog: {}", JSON.toJSONString(apiOprLog));}private void executeAfter(ProceedingJoinPoint proceedingJoinPoint, String id, Object result) {// 获取目标方法的签名信息MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();// 从方法签名中获取 ApiOprLogAnno 注解的信息ApiOprLogAnno apiOprLogAnno = signature.getMethod().getAnnotation(ApiOprLogAnno.class);if (!apiOprLogAnno.isSaveResponse()) {return;}ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setResResult(JSON.toJSONString(result));apiOprLog.setUpdateTime(DateTime.now());// 入库操作log.debug("executeAfter apiOprLog: {}", JSON.toJSONString(apiOprLog));}private void executeAfterEx(Throwable ex, String id) {ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setExMessage(ex.toString());apiOprLog.setExJson(ExceptionUtil.stacktraceToString(ex));apiOprLog.setUpdateTime(DateTime.now());// 入库操作log.debug("executeAfterEx apiOprLog: {}", JSON.toJSONString(apiOprLog));}/*** 封装 ApiOprLog** @param id 主键* @param sourceIp IP* @param method 方法* @param apiOprLogAnno 注解* @return 接口操作日志对象*/private ApiOprLog packaging(String id,String sourceIp,String method,ApiOprLogAnno apiOprLogAnno) {ApiOprLog apiOprLog = new ApiOprLog();apiOprLog.setId(id);apiOprLog.setSourceIp(sourceIp);apiOprLog.setUsername("Regexp");apiOprLog.setMethod(method);apiOprLog.setApiModule(moduleName);apiOprLog.setApiType(apiOprLogAnno.apiType());apiOprLog.setApiDetail(apiOprLogAnno.apiDetail());apiOprLog.setCreateTime(DateTime.now());return apiOprLog;}
}
4. 进行测试
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/get")@ApiOprLogAnno(apiType = "查询", apiDetail = "查询单个用户", isSaveResponse = true)public Person get() {return new Person("Regexp", 18);}@PostMapping("/save")@ApiOprLogAnno(apiType = "保存", apiDetail = "保存单个用户", isSaveRequest = true)public String save(@RequestBody Person person) {log.debug("save person: {}", JSON.toJSONString(person));return "ok";}@GetMapping("/getEx")@ApiOprLogAnno(apiType = "查询", apiDetail = "查询单个用户(异常情况)")public Person getEx() {throw new IllegalArgumentException();}
}
三、问题记录
1. 引用不是注解类型
描述
启动项目时,报错如下:
Caused by: java.lang.IllegalArgumentException: error Type referred to is not an annotation type: cn$regexp$dailylearningtest$anno$ApiOprLog
分析
从报错信息来看,显示为:错误的类型,引用的不是一个注解类型。
Ctrl + Shift + F 全局搜索 ApiOprLog,看看哪些地方有用到 ApiOprLog。
经过搜索,发现在@annotation
中引用了 ApiOprLog(注解重命名后,这里忘记改了),但 ApiOprLog 并不是注解类型,所以导致启动项目时,Spring找到了这个类但这个类却不是注解,就报了这个错。
@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLog)")
public void pointcut(){}
将 ApiOprLog 修改为正确的注解名称即可。
@Pointcut("@annotation(cn.regexp.dailylearningtest.anno.ApiOprLogAnno)")
public void pointcut(){}
2. 依赖冲突
描述
SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found
binding in
[jar:file:/D:/OpenSource/maven-repository/ch/qos/logback/logback-classic/1.2.11/logback-classic-1.2.11.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in
[jar:file:/D:/OpenSource/maven-repository/org/slf4j/slf4j-reload4j/1.7.36/slf4j-reload4j-1.7.36.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an
explanation. SLF4J: Actual binding is of type
[ch.qos.logback.classic.util.ContextSelectorStaticBinder]
分析
从以上信息来看,应该是发生了依赖冲突导致的。
在控制台输入 mvn dependency:tree
查看项目中所有使用的依赖以及依赖中引用的依赖,查找哪些依赖使用了 slf4j,在其中一个依赖中使用exclusions
进行排除即可,如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId></exclusion></exclusions></dependency>
相关文章:

【实战教程】使用Spring AOP和自定义注解监控接口调用
一、背景 随着项目的长期运行和迭代,积累的功能日益繁多,但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。 为了提高系统性能和代码质量,我们往往需要对那些不常用的功能进行下线处理。 那么,该下线哪些功能呢&…...
算法学习之:Raft-分布式一致性/共识算法
基础介绍 Raft是什么? Raft is a consensus algorithm that is designed to be easy to understand. Its equivalent to Paxos in fault-tolerance and performance. The difference is that its decomposed into relatively independent subproblems, and it clea…...

彩色进度条(C语言版本)
.h文件 #include<stdio.h> #include<windows.h>#define NUM 101 #define LOAD_UP 50 #define LOAD_DOWN 60 #define SLEEP_SLOW 300 #define SLEEP_FAST 70 版本1:(初始版) //v1 #include "progress.h" int main() …...
C#和C++有什么区别?
C#和C都是广泛使用的编程语言,但它们在设计理念、应用场景和语法上有许多显著的区别。以下是一些关键区别的详细介绍: 1. 设计理念和目的 C: 设计目的:C是一种面向系统编程和应用程序开发的语言,具有高效性和灵活性…...

微信小程序报错:notifyBLECharacteristicValueChange:fail:nodescriptor的解决办法
文章目录 一、发现问题二、分析问题二、解决问题 一、发现问题 微信小程序报错:notifyBLECharacteristicValueChange:fail:nodescriptor 二、分析问题 这个提示有点问题,应该是该Characteristic的Descriptor有问题,而不能说nodescriptor。 …...
富格林:可信攻略阻止遭遇欺诈
富格林悉知,在投资市场中,如何阻止遭遇欺诈情况应该是每位投资者都想要了解的一个知识点。事实上,现货黄金市场相对来说会其他市场复杂多变,因此要想盈利出金还是得要先学会阻止遭遇欺诈情况。据富格林所知,目前市面上…...

搭建淘宝扭蛋机小程序:技术选型与最佳实践
随着移动互联网的快速发展,小程序作为一种轻量级应用,以其无需安装、即用即走的特点,受到了广大用户的喜爱。在电商领域,淘宝作为国内最大的电商平台之一,也积极拥抱小程序技术,为用户提供更加便捷、个性化…...

【线性回归】梯度下降
文章目录 [toc]数据数据集实际值估计值 梯度下降算法估计误差代价函数学习率参数更新 Python实现导包数据预处理迭代过程结果可视化完整代码 结果可视化线性拟合结果代价变化 数据 数据集 ( x ( i ) , y ( i ) ) , i 1 , 2 , ⋯ , m \left(x^{(i)} , y^{(i)}\right) , i 1 ,…...

GMSL图像采集卡,适用于无人车、自动驾驶、自主机器、数据采集等场景,支持定制
基于各种 系列二代 G MS L 图像采集卡(以下简称 二代图像采集卡)是一款自主研发的一款基于 F P G A 的高速图像产品,二代图像采集卡相比一代卡,由于采用PCIe G en 3 技术,速度和带宽都相应的有了成 倍的提高。该图像…...

docker不删除容器更改其挂载目录
场景:docker搭建的jenkins通常需要配置很多开发环境,当要更换挂载目录,每次都需要删除容器重新运行,不在挂载目录的环境通常不会保留。 先给一个参考博客docker不删除容器,修改容器挂载或其他_jenkins 修改容器挂载do…...

K8s Service 背后是怎么工作的?
kube-proxy 是 Kubernetes 集群中负责服务发现和负载均衡的组件之一。它是一个网络代理,运行在每个节点上, 用于 service 资源的负载均衡。它有两种模式:iptables 和 ipvs。 iptables iptables 是 Linux 系统中的一个用户空间实用程序,用于…...

ClickHouse配置与使用
静态IP配置 # 修改网卡配置文件 vim /etc/sysconfig/network-scripts/ifcfg-ens33# 修改文件内容 TYPEEthernet PROXY_METHODnone BROWSER_ONLYno BOOTPROTOstatic IPADDR192.168.18.128 NETMASK255.255.255.0 GATEWAY192.168.18.2 DEFROUTEyes IPV4_FAILURE_FATALno IPV6INIT…...

将某一个 DIV 块全屏展示
文章目录 需求分析 需求 上节我们研究了如何将页面中的指定 div 下载为图片:跳转查看 本节演技一下如何将 DIV 全屏展示 全屏展示某一个 DIV 分析 其实就是模拟键盘动作 F11 var element document.getElementById(pic) var requestMethod element.requestFullS…...

K8S集群再搭建
前述:总体是非常简单的,就是过程繁琐,不过都是些重复的操作 master成员: [controller-manager, scheduler, api-server, etcd, proxy,kubelet] node成员: [kubelet, proxy] master要修改的配置文件有 1. vi /etc/etcd/etcd.conf # 数…...
工具-博客搭建
以下相关讲解均基于hexo github pages方案,请注意!!!博客搭建方案选择 参考文章1 搭建教程 参考文章1 hexo github pages搭建过程中遇到的问题 删除categories、tags 1、删除含有需要删除categories、tags的文章 2、hexo …...
贪心算法:合并区间
参考资料:代码随想录 题目链接:. - 力扣(LeetCode) 做过用最少数量的箭引爆气球和无重叠区间这两道题目后,题意和题解都不难理解。唯一的一点儿难点是对于api的运用。 class Solution {public int[][] merge(int[][…...

DFA 算法
为什么要学习这个算法 前一段时间遇到了瓶颈,因为词库太多了导致会有一些速度过慢,而且一个正则表达式已经放不下了,需要进行拆分正则才可以。 正好我以前看过有关 dfa 的介绍,但是并没有深入的进行研究,所以就趁着周…...

Web(数字媒体)期末作业
一.前言 1.本资源为类似于打飞机的网页游戏 2.链接如下:【免费】前端web或者数字媒体的期末作业(类似于打飞机的2D网页小游戏)资源-CSDN文库 二.介绍文档...

展现金融科技前沿力量,ATFX于哥伦比亚金融博览会绽放光彩
不到半个月的时间里,高光时刻再度降临ATFX。而这一次,是ATFX不曾拥有的桂冠—“全球最佳在线经纪商”(Best Global Online Broker)。2024年5月15日至16日,拉丁美洲首屈一指的金融盛会—2024年哥伦比亚金融博览会(Money Expo Colombia 2024) 于…...
html 根字号 以及 设置根元素font-size:calc(100vw/18.75)、元素rem实现自适应
rem 单位介绍:rem 是相对文档根元素(html)字体大小的尺寸单位,当元素的尺寸或文字字号等使用 rem 单位时,会随着根元素的 font-size变化而变化。 得出结论:在不同分辨率的设备下动态设置根元素的字体大小就可以实现页面自适应。 …...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...
LeetCode - 199. 二叉树的右视图
题目 199. 二叉树的右视图 - 力扣(LeetCode) 思路 右视图是指从树的右侧看,对于每一层,只能看到该层最右边的节点。实现思路是: 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

【MATLAB代码】基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),附源代码|订阅专栏后可直接查看
文章所述的代码实现了基于最大相关熵准则(MCC)的三维鲁棒卡尔曼滤波算法(MCC-KF),针对传感器观测数据中存在的脉冲型异常噪声问题,通过非线性加权机制提升滤波器的抗干扰能力。代码通过对比传统KF与MCC-KF在含异常值场景下的表现,验证了后者在状态估计鲁棒性方面的显著优…...