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

news/2025/1/16 2:41:58 标签: spring boot, 后端, java

自定义注解使用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 切面代码

java">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

java">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日志入库

java">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.niftyadmin.cn/n/5824575.html

相关文章

excel按行检索(index+match)

假设你的数据表如下&#xff1a; 假设 数据区域是 A1:D4。 你想查询某人在某个日期的数据。 实现步骤 公式 在某个单元格中使用以下公式&#xff1a; excel 复制代码 INDEX(A2:D4, MATCH(“张三”, A2:A4, 0), MATCH(“2025/01/02”, A1:D1, 0)) 2. 公式拆解 MATCH(“张三”,…

常用的前端4种请求方式

文章目录 一、GET请求1.1 使用方式1.2 优缺点1.3 应用场景 二、POST请求2.1 使用方式2.2 优缺点2.3 应用场景 三、PUT请求3.1 使用方式3.2 优缺点3.&#xff13; 应用场景 四、DELETE请求4.1 使用方式4.2 优缺点4.3 应用场景 五.总结 一、GET请求 GET 请求用于向指定资源发出请…

[C++]类与对象(上)

目录 &#x1f495;1.C中结构体的优化 &#x1f495;2.类的定义 &#x1f495;3.类与结构体的不同点 &#x1f495;4.访问限定符(public,private,protected) &#x1f495;5.类域 &#x1f495;6.类的实例化 &#x1f495;7.类的字节大小 &#x1f495;8.类的字节大小特例…

MySQL中的合并函数

一、group_concat MySQL的GROUP_CONCAT函数是一种强大的聚合函数&#xff0c;通常用于将多个行合并为一个字符串。 group_concat(DISTINCT 要连接的字段 Order BY ASC/DESC排序字段 Separator分隔符) 在合并之时对合并的数据排序&#xff0c;可以确定在拆解合并后的字段后&a…

九 RK3568 android11 MPU6500

一 MPU6500 内核驱动 1.1 查询设备连接地址 查看原理图, MPU6500 I2C 连接在 I2C4 上, 且中断没有使用 i2c 探测设备地址为 0x68 1.2 驱动源码 drivers/input/sensors/gyro/mpu6500_gyro.c drivers/input/sensors/accel/mpu6500_acc.c 默认 .config 配置编译了 mpu6550 …

人工智能任务19-基于BERT、ELMO模型对诈骗信息文本进行识别与应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能任务19-基于BERT、ELMO模型对诈骗信息文本进行识别与应用。近日&#xff0c;演员王星因接到一份看似来自知名公司的拍戏邀约&#xff0c;被骗至泰国并最终被带到缅甸。这一事件迅速引发了社会的广泛关注。该…

Oracle EBS GL定期盘存WIP日记账无法过账数据修复

系统环境 RDBMS : 12.1.0.2.0 Oracle Applications : 12.2.6 问题症状 用户反映来源为“定期盘存”和类别为“WIP”的日记账无法过账,标准日记账的界面上的过账按钮灰色不可用。但是,在超级用户职责下,该日记账又可以过账,细心检查发现该业务实体下有二个公司段值15100和…

三小时深度学习PyTorch

【对新手非常友好】三小时深度学习PyTorch快速入门&#xff01;包教会你的&#xff01; --人工智能/深度学习/pytorch_哔哩哔哩_bilibili从头开始&#xff0c;把概率论、统计、信息论中零散的知识统一起来_哔哩哔哩_bilibili从编解码和词嵌入开始&#xff0c;一步一步理解Trans…