【实战教程】使用Spring AOP和自定义注解监控接口调用

news2024/9/21 12:25:29

一、背景

随着项目的长期运行和迭代,积累的功能日益繁多,但并非所有功能都能得到用户的频繁使用或实际上根本无人问津。

为了提高系统性能和代码质量,我们往往需要对那些不常用的功能进行下线处理。

那么,该下线哪些功能呢?

此时,我们就需要对接口的调用情况进行统计和分析了!

二、实战

以下内容为主要代码,完整代码请参考: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>

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

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

相关文章

C从零开始实现贪吃蛇大作战

个人主页&#xff1a;星纭-CSDN博客 系列文章专栏 : C语言 踏上取经路&#xff0c;比抵达灵山更重要&#xff01;一起努力一起进步&#xff01; 有关Win32API的知识点在上一篇文章&#xff1a; 目录 一.地图 1.控制台基本介绍 2.宽字符 1.本地化 2.类项 3.setlocale函…

原生标签WebComponent

文章目录 介绍一、web Component二、怎么使用三、在Vue中使用使用场景 前端必备工具推荐网站(免费图床、API和ChatAI等实用工具): http://luckycola.com.cn/ 介绍 平常浏览各个网站过程中&#xff0c;经常遇到的一种现象&#xff1a;页面广告。 这种广告按照来源可分为两种&…

Python TinyDB库:轻量级NoSQL数据库的终极指南

更多Python学习内容&#xff1a;ipengtao.com TinyDB是一个轻量级的NoSQL数据库&#xff0c;适用于需要嵌入式数据库的小型项目。它使用JSON文件存储数据&#xff0c;并提供了简单易用的API&#xff0c;支持多种查询和索引操作。TinyDB非常适合那些不需要复杂数据库功能的小型应…

C++Qt操作Lotus Domino数据库 Lotus Domino C++连接Lotus Domino C++快速开发Lotus Domino

java连接domino C#连接domino python连接domino go连接domino,delphi连接domino Excel连接domino Flutter、微信小程序连接domino C 操作 Lotus Domino 数据库&#xff1a;自动化与效率的结合 引言 在企业级应用中&#xff0c;Lotus Domino 提供了一个强大的协作平台&#xff0…

【吊打面试官系列】Java高并发篇 - ThreadLocal 是什么?有什么用?

大家好&#xff0c;我是锋哥。今天分享关于 【ThreadLocal 是什么&#xff1f;有什么用&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; ThreadLocal 是什么&#xff1f;有什么用&#xff1f; ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该…

2022年CSP-J入门级第一轮初赛真题

一、单项选择题&#xff08;共15题&#xff0c;每题2分&#xff0c;共计30分&#xff1b;每题有且仅有一个正确选项&#xff09; 第 1 题 在内存储器中每个存储单元都被赋予一个唯一的序号&#xff0c;称为&#xff08;&#xff09;。 A. 地址B. 序号C. 下标D. 编号 第 2 题 编…

电脑怎么录屏?电脑录屏的7个方法,仅3%的人知道!

你知道电脑怎么录屏吗&#xff1f;在电脑上录屏是向朋友展示炫酷游戏技巧、制作软件教程视频和展示数字艺术技巧的好方法。遗憾的是&#xff0c;屏幕录制并不像截屏那么简单。然而&#xff0c;无论你是在寻找在电脑上录制屏幕&#xff0c;亦或是录制音频的方法&#xff0c;还是…

C/C++|malloc分配内存详解

看本节前&#xff0c;希望读者有linux内存分布的基本概念&#xff0c;可以阅读这篇文章&#xff1a; 进程虚拟地址空间和函数调用栈 在本节中希望读者可以一口气阅读完所有内容。 本博客内容全部来自小林coding&#xff1a;malloc 是如何分配内存的&#xff1f; 这里仅为笔记记…

基于docxtpl的模板生成Word

docxtpl是一个用于生成Microsoft Word文档的模板引擎库。它结合了docx模块和Jinja2模板引擎&#xff0c;使用户能够使用Microsoft Word模板文件并在其中填充动态数据。这个库提供了一种方便的方式来生成个性化的Word文档&#xff0c;并支持条件语句、循环语句和变量等控制结构&…

Mycat+Mysql搭建数据集群实现数据分片存储

前言 MyCAT介绍 * 一个彻底开源的,面向企业应用开发的“大数据库集群”; * 支持事务、ACID、可以替代MySQL的加强版数据库; * 一个可以视为“MySQL”集群的企业级数据库,用来替代昂贵的Oracle集群; * 一个融合内存缓存技术、Nosql技术、HDFS大数据的新型SQL; * 一个新颖…

天诚公租房/人才公寓WiFi人脸识别物联网智能门锁解决方案

人才是引领城市高质量发展的重要因素&#xff0c;城市要想吸纳人才的保障便是人才公寓。近年来&#xff0c;全国各地一二三线城市都在大力建设人才公寓&#xff0c;集聚菁英人才&#xff0c;倾力打造人才高地。 一、人才公寓如火如荼建设 2023年底&#xff0c;山东德州提出三年…

安装termux遇到的问题-逍遥模拟器

问题一 做一次更新即可解决问题&#xff0c;pkg update 问题二 sshd&#xff1a;no hostkeys available -- exiting. 尝试了网上提供的方法 ssh-keygen -t rsa -f /data/data/com.termux/files/usr/etc/ssh/ssh_host_rsa_key ssh-keygen -t dsa -f /data/data/com.termux/…

iOS swift5 提示信息显示,提示弹框,第三方框架XHToastSwift

文章目录 1.github地址(亲测好用)2.源代码 1.github地址(亲测好用) XHToastSwift - github 2.源代码 XHToast.swift // // XHToast.swift // XHToastSwiftExample // // Created by xiaohui on 16/8/12. // Copyright © 2016年 CoderZhuXH. All rights reserved. …

坦克飞机大战游戏开发:深入解析角色与功能类创建

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、角色创建概览 角色属性与方法的实现 二、公用类与功能性类的封装 公用类的特点与应用…

建设现代智能工业-智能化、数字化、自动化节能减排

建设现代智能工业-智能化节能减排 遵循“一体化”能源管理(Integrated Energy Management)的设计宗旨&#xff0c;集成城市各领域(如工业.交通、建筑等&#xff09;的能源生产和消费信息&#xff0c;面向城市政府、企业、公众三类实体&#xff0c;提供“一体化”的综合能源管理…

河道流量监测解决方案

河道流量监测解决方案 河道流量的远程监测是现代水资源管理与防洪减灾体系中的关键技术环节&#xff0c;它依赖于物联网&#xff08;Internet of Things, IoT&#xff09;技术的深度整合与应用&#xff0c;旨在实现对河流水文动态的实时、准确、高效监测。该解决方案不仅提升了…

嵌入式实时操作系统笔记2:UCOS基础知识_UC/OS-III移植(STM32F4)_编写简单的UC/OS-III任务例程(失败.....)

今日学习嵌入式实时操作系统RTOS&#xff1a;UC/OS-III实时操作系统 本文只是个人学习笔记备忘用&#xff0c;附图、描述等 部分都是对网上资料的整合...... 文章主要研究如何将UC/OS-III 移植到 STM32 F407VET6上&#xff0c;提供测试工程下载 &#xff08;2024.5.21 文章未…

上门服务系统开发|东邻到家系统|上门服务系统开发流程

上门服务小程序的开发流程是一个复杂且精细的过程&#xff0c;涉及到需求分析、设计规划、开发实施、测试验收以及上线运营等多个环节。下面将详细介绍上门服务小程序的开发流程&#xff0c;帮助读者全面了解并掌握其中的关键步骤。 一、需求分析 在开发上门服务小程序之前&am…

北核论文完美复现:自适应t分布与动态边界策略改进的算术优化算法

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 目录 原始算术优化算法 改进点1&#xff1a;引入…