自定义注解使用AspectJ切面和SpringBoot的Even事件优雅记录业务接口及第三方接口调用日志实现思路

news2025/5/21 13:32:14

自定义注解使用AspectJ切面和SpringBoot的Even事件优雅记录业务接口及第三方接口调用日志实现思路

文章目录

  • 1.前言
  • 2.思路
    • 2.1使用ELK收集日志
      • 2.1.1ELK搭建
        • 2.1.2项目中集成ELK日志收集
        • 2.1.2.1 引入依赖
        • 2.1.2.2 logback-xxx.xml配置
        • 2.1.2.3 yaml配置
    • 2.2本文思路
      • 2.2.1书接上文--自定义注解之AspectJ切面动态代理使用注意事项
      • 2.2.2 切面代码
      • 2.2.3xxxReqLogEvent
      • 2.2.4BizListener日志入库
      • 2.2.5接口调用日志表设计
  • 3.业务接口 + 第三方接口调用姿势
  • 4.总结

1.前言

  在日常的开发中经常会遇到对接第三方系统,如:各种支付(微信支付、支付宝支付。易宝支付,抖音支付、京东支付、美团支付、银联支付、云闪付等)、ocr识别(阿里、旷世等)、各种短信验证方接口、隐私号码打电话(华为、阿里)、开发票(百望等,税控盘发票或数电发票)等等,这些第三方都是通过sdk或者是https或者是http的方式提供一个json格式的接口,它们都有一个自己的开放平台,接入都需要使用应用appId和accessKey、secretKey等,有的使用RSA加解密及参数验签,有的使用SM4对参数和响应进行加解密,有的使用证书对参数和响应进行加解密及验签,有的使用其它加密和解密算法对参数进行加解密及验签啥的,大体上都是一个套路,都是通过http或者https协议加上一些加密算法实现,有的提供了好用的sdk,很方便使用,有的需要集成方自己写代码实现接口调用,这种方式就很low的。

  假如你写了一个支付服务、开发票服务、ocr识别服务等此类通用的服务,提供给公司内部其他业务使用,

  那此时你写的这些通用服务就相当于一个服务提供方,业务方来调用你的接口,你的接口又去调用第三方的接口,此时,如果调用中出现了一些问题,报错了导致接口不通,你该如何去排查分析定位到问题呢?如果服务应用重启或者重启了容器,没有使用elk,也没有配置日志输出到服务路径,此时重启之后就没有历史的日志了,如果集成了ELK等日志收集,项目中集成了ELK的相关依赖及配置,但是ELK存储日志也只是存储一段时间的,不是永久存储,否则磁盘不够用,所以定期要去清理ELK中存储了很久的日志,如果一个问题是很久的时候发生的,现在才反馈,去ELK中已经查不到日志了,此时,对于排查问题就很难排查,没有日志分析定位问题的难度是很大的,只能去看代码猜测问题,或者是根据前端返回异常信息看看是否能看出蛛丝马迹,还有一种是把生产的各个阶段的数据拿到测试环境从数据源头、数据扭转、在测试环境复现生产异常,这个方法有的时候还是管用的,但是就是实现起来很有难度,那这种难搞,那 有没有什么好的方法来解决这个问题呢?首先,日志可以永久存储,还可以记录到异常信息或者是业务处理抛出的业务异常信息,入参、出参,请求头,请求体,响应体,加密报文以及解密报文,业务方法层面的入参、出参及业务方法处理层面抛出的异常等信息,这种持久化到数据库的表中如果有问题,后续排查问题既方便又快捷的方法有没有有呢?答案是有的,我最近就实践出了一个好的思路,请看下文分解。

2.思路

2.1使用ELK收集日志

2.1.1ELK搭建

  省略,这个不是本文的重点,可以去网上搜索相关教程。

2.1.2项目中集成ELK日志收集
2.1.2.1 引入依赖
      <properties>
          <skywalking.version>8.4.0</skywalking.version>
       </properties>

       <dependencies>
           <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-logback-1.x</artifactId>
            <version>${skywalking.version}</version>
        </dependency>
        <dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
            <version>6.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.skywalking</groupId>
            <artifactId>apm-toolkit-trace</artifactId>
            <version>${skywalking.version}</version>
        </dependency>
      </dependencies>
2.1.2.2 logback-xxx.xml配置

  logback-xxx.xml中的xxx是对应激活那个环境配置,有测试环境、生产环境等

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty name="spring.application.name" scope="context" source="spring.application.name"/>
    <springProperty scope="context" name="elkLoggerUrl" source="elk.logger.destination"/>

    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%tid){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"/>

    <!--add converter for %tid -->
    <conversionRule conversionWord="tid"
                    converterClass="org.apache.skywalking.apm.toolkit.log.logback.v1.x.LogbackPatternConverter"/>


    <appender name="logstash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>${elkLoggerUrl}</destination>
        <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <pattern>
                    <pattern>
                        {
                        "level": "%level",
                        "serviceName": "${spring.application.name:-}",
                        "pid": "${PID:-}",
                        "tid": "%tid",
                        "thread": "%thread",
                        "class": "%logger{1.}",
                        "message": "%message",
                        "stackTrace": "%exception{10}"
                        }
                    </pattern>
                </pattern>
            </providers>
        </encoder>
    </appender>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            </layout>
        </encoder>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <!--        <neverBlock>true</neverBlock>-->
        <appender-ref ref="logstash"/>
    </appender>
     <!--xxx.xxxx.xxxx 为项目中的包路径-->
    <logger name="xxx.xxxx.xxxx" level="INFO">
        <appender-ref ref="ASYNC"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="console"/>
    </root>
</configuration>
2.1.2.3 yaml配置

  application.yaml 或者 bootstrap.yml等,或者是nacos上的配置

spring:
  application:
    name: xxx #项目名称
  profiles:
    active: xxx #激活环境
elk:
  logger:
    destination: ip:920 #es地址:端口
logging:
  level:
#   root: info
# 可以指定多个报名路径的日志级别
    xxxxx.xxx.xxx: info
  # 这里是指定logback日志配置文件位置,就是2.1.2.2 logback-xxx.xml文件(该文件在工程目录的resources下)
  config: classpath:logback-xxx.xml

2.2本文思路

2.2.1书接上文–自定义注解之AspectJ切面动态代理使用注意事项

https://mp.weixin.qq.com/s/99IUB23Ba-ynuU-hs3giDg
https://blog.csdn.net/qq_34905631/article/details/145148423?spm=1001.2014.3001.5501

2.2.2 切面代码

package xxxx.xxxx.annotation;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Objects;

@Slf4j
@Aspect
@Component
public class xxxRequestLogAspect {

    @Autowired
    private ApplicationContext applicationContext;

    //切面表达式的写法还有很多种写法,这个只是其中一种
    @Pointcut("@annotation(xxx.xxxx.xxx.xxxRequestLogAspect)")
    public void xxxRequestLogPoint() {

    }

    @Around("xxxRequestLogPoint()")
    public xxxx deal(ProceedingJoinPoint pjp) throws Throwable {
        //当前线程名
        String threadName = Thread.currentThread().getName();
        log.info("-------------RequestLogAspect开始执行-----线程:{}-----------", threadName);
        Exception exception = null;
        //获取参数列表
        Object[] objs = pjp.getArgs();
        String message = "";
        xxxReq xxxReq = null;
        xxxRequestLogAnno annotation = null;
        xxxLogDto xxxReqLogDto = new xxxxReqLogDto();
        try {
            MethodSignature ms = (MethodSignature) pjp.getSignature();
            Method method = ms.getMethod();
            String methodName = method.getName();
            String classSimpleName = method.getClass().getSimpleName();
            //获取第一个参数
            xxxReq = (xxxReq) objs[0];
            log.info("classSimpleName:{}.methodName:{},xxxReq:{}", classSimpleName, methodName, JSON.toJSONString(xxxReq));
            if (Objects.isNull(xxxReq)) {
                throw new RuntimeException("接口参数不为空");
            }
            String appId = xxxReq.getAppId();
            if (StringUtils.isEmpty(appId)) {
                throw new RuntimeException("接口参数中appId不为空");
            }
            xxxReqLogDto.setAppId(appId);
            //获取该注解的实例对象,暂时没有用到注解属性控制逻辑
            annotation = ((MethodSignature) pjp.getSignature()).
                    getMethod().getAnnotation(xxxRequestLogAnno.class);
            // 记录开始时间
            long startTime = System.currentTimeMillis();
            // 记录结束时间
            xxxResp xxResp = (xxxesp) pjp.proceed();
            if (Objects.nonNull(xxxResp)) {
                xxxReqLogDto.setRequest(JSON.toJSONString(xxxReq.getRequestMaps()));
                if (xxResp.getIsSuccess()) {
                    //接口调用成功
                    xxxReqLogDto.setStatus("success");
                } else {
                    Error error = xxResp.getError();
                    if (Objects.nonNull(error)) {
                        log.info("classSimpleName:{}.methodName:{},响应Error:{}", classSimpleName, methodName, JSON.toJSONString(error));
                    }
                    xxxReqLogDto.setStatus("fail");
                }
                xxxReqLogDto.setResponse(JSON.toJSONString(xxResp));
            }
            long endTime = System.currentTimeMillis();
            // 计算耗时
            long duration = endTime - startTime;
            xxxReqLogDto.setCostTime(duration);
            log.info("classSimpleName:{}.methodName:{},xxResp:{},duration:{}毫秒", classSimpleName, methodName, JSON.toJSONString(xxResp), duration);
            log.info("RequestLogAspect发送ReqLogEvent事件开始,ReqLogDto:{}", JSON.toJSONString(xxxReqLogDto));
            xxxReqLogEvent xxxReqLogEvent = new xxxReqLogEvent(this, xxxReqLogDto);
            applicationContext.publishEvent(xxxReqLogEvent);
            log.info("RequestLogAspect发送ReqLogEvent事件完成");
            return xxxResp;
        } catch (Exception e) {
            exception = e;
            message = e.getMessage();
            String stackTrace = ExceptionUtils.getStackTrace(e);
            log.error("-------------RequestLogAspect.message:{},stackTrace:{}-----线程{}-----------", message, stackTrace, threadName);
            xxxReqLogDto.setRequest(JSON.toJSONString(xxxReq));
            if (StringUtils.isNotBlank(message)) {
                if (message.length() > 255) {
                    xxxReqLogDto.setExMsg(message.substring(0, 255));
                } else {
                    xxxReqLogDto.setExMsg(message);
                }
            } else if (StringUtils.isEmpty(message)) {
                if (StringUtils.isNotBlank(stackTrace)) {
                    if (stackTrace.length() > 255) {
                        xxxReqLogDto.setExMsg(stackTrace.substring(0, 255));
                    } else {
                        xxxReqLogDto.setExMsg(stackTrace);
                    }
                }
            }
            xxxReqLogDto.setStatus("fail-error");
            log.info("异常处理中===>RequestLogAspect发送ReqLogEvent事件开始,ReqLogDto:{}", JSON.toJSONString(xxxReqLogDto));
            xxxReqLogEvent xxxReqLogEvent = new xxxReqLogEvent(this, xxxReqLogDto);
            applicationContext.publishEvent(xxxReqLogEvent);
            log.info("异常处理中===>RequestLogAspect发送ReqLogEvent事件完成");
        }
        if (StringUtils.isNotBlank(message)) {
            throw new RuntimeException(message.replaceAll("RuntimeException", "").replaceAll("Exception", "").replaceAll(":", "").replaceAll(" ", ""));
        }
        throw new RuntimeException(exception);
    }

}

2.2.3xxxReqLogEvent

package xxxx.xxxx.xx.event;

import xxx.xxx.xxxReqLogDto;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class xxxReqLogEvent extends ApplicationEvent {

    private xxxReqLogDto xxxReqLogDto;

    public xxxReqLogEvent(Object source, xxxReqLogDto xxxReqLogDto) {
        super(source);
        this.xxxReqLogDto = xxxReqLogDto;
    }

}

2.2.4BizListener日志入库

package xxx.xxxx.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class BizListener {

    @Autowired
    private xxxxRequestLogService xxxxRequestLogService;


    @EventListener
    public void xxxReqLogListener(xxxReqLogEvent xxxReqLogEvent) {
        //接口api调用日志入库
        Boolean result = xxxRequestLogService.saveRequestLog(xxxReqLogEvent.getXxxReqLogDto());
        log.info("ReqLogListener保存接口api调用日志入库完成result:{}", result);
    }


}

2.2.5接口调用日志表设计

图片

  这个接口调用日志表可以根据自己对接的第三方接口或者是根据自己的业务系统来设计即可。

3.业务接口 + 第三方接口调用姿势

  根据以上的原理(套路),可以在业务层接口在搞一层切面,标记业务接口的调用日志也入库,这种就可以知道业务接口本次调用是有啥异常或者是不满足什么条件抛出的业务异常等信息入库,查问题就非常方便了的。

图片

4.总结

  以上是最近写项目的一个思路,也是之前写项目,一个接口调用里面写一遍相同重复的接口日志记录代码,这种方式可以简化代码,提高排查问题的效率,可以精准记录问题日志,本次分享到此结束,希望我的分享对你有所启发和帮助,请一键三连,么么么哒!

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

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

相关文章

Windows 正确配置android adb调试的方法

下载适用于 Windows 的 SDK Platform-Tools https://developer.android.google.cn/tools/releases/platform-tools?hlzh-cn 设置系统变量&#xff0c;路径为platform-tools文件夹的绝对路径 点击Path添加环境变量 %adb%打开终端输入adb shell 这就成功了&#xff01;

保姆级图文详解:Linux和Docker常用终端命令

文章目录 前言1、Docker 常用命令1.1、镜像管理1.2、容器管理1.3、网络管理1.4、数据卷管理1.5、监控和性能管理 2、Linux 常用命令分类2.1、文件和目录管理2.2、用户管理2.3、系统监控和性能2.4、软件包管理2.5、网络管理 前言 亲爱的家人们&#xff0c;技术图文创作很不容易…

相机SD卡照片数据不小心全部删除了怎么办?有什么方法恢复吗?

前几天&#xff0c;小编在后台友收到网友反馈说他在整理相机里的SD卡&#xff0c;原本是想把那些记录着美好瞬间的照片导出来慢慢欣赏。结果手一抖&#xff0c;不小心点了“删除所有照片”&#xff0c;等他反应过来&#xff0c;屏幕上已经显示“删除成功”。那一刻&#xff0c;…

《C++11》nullptr介绍:从NULL说起

在C11之前&#xff0c;我们通常使用NULL来表示空指针。然而&#xff0c;NULL在C中有一些问题和限制&#xff0c;这就是C11引入nullptr的原因。本文将详细介绍nullptr的定义、用法和优点。 1. NULL的问题 在C中&#xff0c;NULL实际上是一个整数0&#xff0c;而不是一个真正的…

【搭建JavaEE】(2)Tomcat安装配置和第一个JavaEE程序

Tomcat–容器(Container) 下载 Apache Tomcat - Welcome! 下载完成 请求/响应 结构 测试 查看Jdk版本 改端口号localhost8080–>8099 学学人家以后牛逼了可以用自己名字当文件夹名 配置端口8099 找到server文件 用记事本打开 再打开另一个logging文件 ”乱码解决“步骤&…

​​​​​​​​​​​​​​★3.3 事件处理

★3.3.1 ※MouseArea Item <-- MouseArea 属性 acceptedButtons : Qt::MouseButtons containsMouse : bool 【书】只读属性。表明当前鼠标光标是否在MouseArea上&#xff0c;默认只有鼠标的一个按钮处于按下状态时才可以被检测到。 containsPress : bool curs…

【C】初阶数据结构3 -- 单链表

之前在顺序表那一篇文章中&#xff0c;提到顺序表具有的缺点&#xff0c;比如头插&#xff0c;头删时间复杂度为O(n)&#xff0c;realloc增容有消耗等。而在链表中&#xff0c;这些问题将得到解决。所以在这一篇文章里&#xff0c;我们将会讲解链表的定义与性质&#xff0c;以及…

Python----Python高级(函数基础,形参和实参,参数传递,全局变量和局部变量,匿名函数,递归函数,eval()函数,LEGB规则)

一、函数基础 1.1、函数的用法和底层分析 函数是可重用的程序代码块。 函数的作用&#xff0c;不仅可以实现代码的复用&#xff0c;更能实现代码的一致性。一致性指的是&#xff0c;只要修改函数的代码&#xff0c;则所有调用该函数的地方都能得到体现。 在编写函数时&#xf…

《leetcode-runner》如何手搓一个debug调试器——架构

本文主要聚焦leetcode-runner对于debug功能的整体设计&#xff0c;并讲述设计原因以及存在的难点 设计引入 让我们来思考一下&#xff0c;一个最简单的调试器需要哪些内容 首先&#xff0c;它能够接受用户的输入 其次&#xff0c;它能够读懂用户想让调试器干嘛&#xff0c;…

食堂采购系统源码:基于PHP的校园食堂供应链管理平台开发全解析

传统的食堂采购管理普遍存在信息不透明、流程繁琐、效率低下等问题&#xff0c;这使得开发一款高效、智能的食堂采购系统变得尤为重要。本篇文章&#xff0c;笔者将详细解析基于PHP开发的校园食堂供应链管理平台&#xff0c;从功能设计、系统架构到技术实现&#xff0c;全方位剖…

WEB 攻防-通用漏-XSS 跨站脚本攻击-反射型/存储型/DOMBEEF-XSS

XSS跨站脚本攻击技术&#xff08;一&#xff09; XSS的定义 XSS攻击&#xff0c;全称为跨站脚本攻击&#xff0c;是指攻击者通过在网页中插入恶意脚本代码&#xff0c;当用户浏览该网页时&#xff0c;恶意脚本会被执行&#xff0c;从而达到攻击目的的一种安全漏洞。这些恶意脚…

重生之我在21世纪学C++—函数与递归

一、函数是什么&#xff1f; 相信我们第一次接触函数是在学习数学的时候&#xff0c;比如&#xff1a;一次函数 y kx b &#xff0c;k 和 b 都是常数&#xff0c;给一个任意的 x &#xff0c;就会得到一个 y 值。 其实在 C 语言中就引入了函数&#xff08;function&#xf…

Mac——Cpolar内网穿透实战

摘要 本文介绍了在Mac系统上实现内网穿透的方法&#xff0c;通过打开远程登录、局域网内测试SSH远程连接&#xff0c;以及利用cpolar工具实现公网SSH远程连接MacOS的步骤。包括安装配置homebrew、安装cpolar服务、获取SSH隧道公网地址及测试公网连接等关键环节。 1. MacOS打开…

Ubuntu中双击自动运行shell脚本

方法1: 修改文件双击反应 参考: https://blog.csdn.net/miffywm/article/details/103382405 chmod x test.sh鼠标选中待执行文件&#xff0c;在窗口左上角edit菜单中选择preference设计双击执行快捷键&#xff0c;如下图&#xff1a; 方法2: 设置一个应用 参考: https://blo…

力扣 全排列

回溯经典例题。 题目 通过回溯生成所有可能的排列。每次递归时&#xff0c;选择一个数字&#xff0c;直到选满所有数字&#xff0c;然后记录当前排列&#xff0c;回到上层时移除最后选的数字并继续选择其他未选的数字。每次递归时&#xff0c;在 path 中添加一个新的数字&…

arcgis提取不规则栅格数据的矢量边界

效果 1、准备数据 栅格数据:dem或者dsm 2、栅格重分类 分成两类即可 3、新建线面图层 在目录下选择预先准备好的文件夹,点击右键,选择“新建”→“Shapefile”,新建一个Shapefile文件。 在弹出的“新建Shapefile”对话框内“名称”命名为“折线”,“要素类型”选…

【DB-GPT】开启数据库交互新篇章的技术探索与实践

一、引言&#xff1a;AI原生数据应用开发的挑战与机遇 在数字化转型的浪潮中&#xff0c;企业对于智能化应用的需求日益增长。然而&#xff0c;传统的数据应用开发方式面临着诸多挑战&#xff0c;如技术栈复杂、开发周期长、成本高昂、难以维护等。这些问题限制了智能化应用的…

LVGL移植高通点阵字库GT30L24A3W

字库芯片: GT30L24A3W MCU:STM32F429 LVGL版本:V8.4 一、实现gt_read_data() 和 r_dat_bat() 请参考下面视频 如何在32位MCU上使用高通点阵字库_哔哩哔哩_bilibili 高通字库使用教程(1)硬件链接与注意事项部分_哔哩哔哩_bilibili 高通字库使用教程(2)SPI底层函数使用_哔哩…

一键掌握多平台短视频矩阵营销/源码部署

短视频矩阵系统的介绍与应用 随着数字化营销策略的不断演进&#xff0c;传统的短视频矩阵操作方法可能已显陈旧。为此&#xff0c;一款全新的短视频矩阵系统应运而生&#xff0c;它通过整合多个社交媒体账户、创建多样化的任务、运用先进的智能视频编辑工具、实现多平台内容的…

MySQL(高级特性篇) 06 章——索引的数据结构

一、为什么使用索引 索引是存储引擎用于快速找到数据记录的一种数据结构&#xff0c;就好比一本教科书的目录部分&#xff0c;通过目录找到对应文章的页码&#xff0c;便可快速定位到需要的文章。MySQL中也是一样的道理&#xff0c;进行数据查找时&#xff0c;首先查看查询条件…