为了理直气壮怼回去,写了一个日志切面输出接口出入参数

news2024/11/26 8:31:01

1.背景

我们在日常排查问题过程中知道,入参传错是导致接口调用失败的常见原因之一。特别是提供给第三方调用的回调接口和openAPI接口,由于无法保证第三方开发人员的水平,经常问题不断,反反复复找你问为啥掉不通,甚至吐槽写的“啥玩意接口”,这时候你肯定一脸懵逼,怒火中烧,想展开撕逼甩锅大战,但是对方有可能是甲方金主爸爸并且你没有第一时间掌握证据证明证是对方调用的问题,你只能忍着问他是如何调接口的,卑微请求他把传参发过来看看。。。为了扭转局势,挺直腰杆怼回去:能不能靠谱点?今天我们就来讲讲系统服务中如何优雅地实现统一打印接口API参数日志,方便服务端开发快速甩锅还能拿出证据!!!

当然需要注意的是,日志打印太多对性能是有一定影响的,并且如果使用ELK架构收集分布式系统日志,也会加大日志收集同步的压力,甚至造成高延迟。所以我们打印日志得分环境和接口的重要程度,一般只需要在测试环境开启,这样第三方联调时候肯定先在测试环境联调通过,最后才正式调线上环境的接口,当然有些接口很重要,排查问题凭证在线上也需要保留,这时候线上的接口参数日志也需要打印着。这就要求我们需要做到:打印参数日志的环境和接口都可以配置指定,主打的就是一个动态灵活性。

话不多说,接下来就来看看我们如何实现这个功能需求

2.实现

说到统一实现,肯定就想到了切面处理。面向切面编程是一种编程范式,它作为 OOP 面向对象编程的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理权限控制缓存控制日志打印等等。在讲切面实现打印日志之前,先来看看动态灵活可配置性咋实现?

2.1 控制环境是否打印接口参数日志

这个实现起来比较简单,我们只需要在配置文件里面设置一个是否开启打印接口参数日志的开关即可:

@Data
@ConfigurationProperties(prefix = "ptc.apiLog")
public class ApiLogProperties {
    /**
     * 是否开启接口日志打印, 默认
     */
    private Boolean enable = Boolean.FALSE;
}

这里默认为false表示不开启接口参数日志打印,然后在自动配置类中使用:

@Configuration
@EnableConfigurationProperties({ApiLogProperties.class})
@PropertySource("classpath:/web-default.properties")
public class PlasticeneWebAutoConfiguration {
    /**
     * 注入api 日志打印拦截器
     * @return
     */
    @Bean
    // @ConditionalOnProperty(name = "ptc.apiLog.enable", havingValue = "true")
    public ApiLogPrintAspect apiLogPrintAspect() {
        return new ApiLogPrintAspect();
    }
}

注意这里注入ApiLogPrintAspect并没有使用条件装配,条件装配代码已经被注释掉了,这样做的原因是,我们在下面切面类进行开关判断,这样可以借助于动态配置更新开关接口参数打印,就不需要重启服务了。还有其实上面的的属性配置类不一定需要,因为我们也可以根据ptc.api.log.enable直接注入获取value值,这中属性配置类一般针对一个功能点下有多个配置才适用一点,我这里写了是为了实现Spring Boot业务系统中自定义配置自动提示:

关于如何实现Spring Boot业务系统中自定义配置自动提示不清楚的可以自行查阅资料了解。

2.2 控制接口是否打印参数日志

这种需要指定某个接口是否需要打印参数日志的动态配置都需要借助于注解实现:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface ApiLog {
}

这个注解定义很简单,连属性都没有,因为只需要在接口方法或所在类打上标识即可,注解可以作用于类上或者接口方法上,作用于类上时意味着该类下的所有方法都需要打印接口参数日志哦。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

2.3 AOP切面实现日志打印

这里使用AOP切面使用此功能,当然也可以使用过滤器filter实现日志切面的效果,但是注意是用filter时候,对入参的获取要谨慎处理,通过request.getInputStream(或者getReader)读取请求数据流(能解析出content-Type为 application/x-www-form-urlencoded ,application/json , text/xml这三种提交方式的数据(注:multipart/form-data不行),但是!!!在后续controller接口中无法获取该数据流。主要原因在于不能在过滤器中读取一次二进制流(字符流),又在另外一个Servlet中读取一次,即一个InputSteam(BufferedReader)对象在被读取完成后,将无法再次被读取。这个我们在之前的总结中分析过,详见:Spring Boot如何实现接口参数加解密。回到正题来看看切面逻辑:

@Aspect
@Slf4j
@Order(value = OrderConstant.AOP_API_LOG)
public class ApiLogPrintAspect {

    @Resource
    private ApiLogProperties apiLogProperties;


    /**
     * 声明切点
     *
     * @param joinPoint 切入点
     *表达式示例:
     * 任意公共方法的执行:execution(public * *(..))
     * 任何一个以“set”开始的方法的执行:execution(* set*(..))
     * AccountService 接口的任意方法的执行:execution(* com.xyz.service.AccountService.*(..))
     * 定义在service包里的任意方法的执行: execution(* com.xyz.service.*.*(..))
     * 定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service..*.*(..))
     * @return 返回值
     * @throws Throwable 异常
     */
    @Around("execution(* com.plasticene..controller..*(..))")
    public Object timeAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 这里做开关判断,而不是根据开关条件注入切面bean,是因为为了方便修改配置开关动态更新来控制开关打印接口参数日志
        if (!apiLogProperties.getEnable()) {
            return joinPoint.proceed();
        }
        ApiLog apiLog = getApiLog(joinPoint);
        if (Objects.isNull(apiLog)) {
            return joinPoint.proceed();
        }
        long start = System.currentTimeMillis();
        HttpServletRequest request = getRequest();
        RequestInfo requestInfo = new RequestInfo();
        requestInfo.setIp(request.getRemoteAddr());
        requestInfo.setUrl(request.getRequestURL().toString());
        requestInfo.setHttpMethod(request.getMethod());
        requestInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName()));
        requestInfo.setRequestParams(getRequestParams(joinPoint, request));
        log.info("Request Info : {}", JsonUtils.toJsonString(requestInfo));

        Object result = joinPoint.proceed();

        log.info("Response result:  {}", JsonUtils.toJsonString(result));
        log.info("time cost:  {}", System.currentTimeMillis() - start);
        return result;
    }

    private Object getRequestParams(ProceedingJoinPoint joinPoint, HttpServletRequest request) throws UnsupportedEncodingException {
        Object[] args = joinPoint.getArgs();
        Object params = null;
        String queryString = request.getQueryString();
        String method = request.getMethod();
        if (args.length > 0) {
             // 有body的接口类型,这时候要排除HttpServletRequest request, HttpServletResponse response作为接口方法参数
            if ("POST".equals(method) || "PUT".equals(method) || "DELETE".equals(method)) {
                int length = args.length;
                int index = 0;
                Object object = null;
                while (index < length) {
                    Object o = args[index];
                    index++;
                    if (o instanceof HttpServletRequest || o instanceof HttpServletResponse) {
                        continue;
                    } else {
                        object = o;
                        break;
                    }
                }
                if (object instanceof MultipartFile) {
                    MultipartFile multipartFile = (MultipartFile) object;
                    params = MessageFormat.format("文件名: {0}, 大小: {1}", multipartFile.getOriginalFilename(), multipartFile.getSize());
                } else {
                    params = object;
                }
                // 方法为get时,当接口参数为路径参数,那么此时queryString为null
            } else if ("GET".equals(method) && StrUtil.isNotBlank(queryString)) {
                params = URLDecoder.decode(queryString, "utf-8");
            }
        }
        return params;
    }


    private HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        return request;
    }

    private ApiLog getApiLog(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        ApiLog apiLog = method.getAnnotation(ApiLog.class);
        if (Objects.isNull(apiLog)) {
            apiLog = method.getDeclaringClass().getAnnotation(ApiLog.class);
        }
        return apiLog;
    }

}

可以看到我们对接口入参进行一定的解析适配,AOP的切入点execution(* com.plasticene..controller..*(..)),必须满足这个包路径才执行切面逻辑,这符合一个公司的项目服务结构规范,以相同前缀为包名方便后续做相关统一拦截逻辑处理,紧接着判断了日志开关和是否打上了@ApiLog标记,最后对接口请求参数和响应参数进行日志打印,这里的日志打印只在一行中打印,也没有什么格式,这样可以有效减少日志量,因为我们平时查日志肯定都是根据traceId来查询一次请求的参数打印,查到之后直接copy出来找一个在线JSON解析工具就可以清楚看到日志信息了。同时这里使用@Order(value = OrderConstant.AOP_API_LOG)控制切面组件的执行顺序, 通过@Order(i)注解来指定优先级,注意:i 值越小,优先级则越高

2.4 使用示例

在业务项目服务的配置文件开启打印日志开关

ptc:
  api:
    log:
      enable: true

编写测试接口:

  @PostMapping("/test/api/log")
  @ApiLog
  public User printApiLog(@RequestBody User user) throws InterruptedException {
      log.info("api log打印啦....");
      TimeUnit.SECONDS.sleep(1);
      return user;
  }

接口调用:

控制台日志打印如下:

[1719387065344552960] [INFO ] [2023-11-01 00:13:16.007] [http-nio-18888-exec-1@29351]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Request Info : {"ip":"127.0.0.1","url":"http://127.0.0.1:18888/fds/test/test/api/log","httpMethod":"POST","classMethod":"com.plasticene.fast.controller.TestController.printApiLog","requestParams":{"createTime":null,"updateTime":null,"creator":null,"updater":null,"id":123,"username":null,"password":null,"name":"she哈哈","mobile":"1234556457","gender":1,"status":null,"email":"shepherd@163.com","remark":"测试打印api日志啦"}}
[1719387065344552960] [INFO ] [2023-11-01 00:13:16.073] [http-nio-18888-exec-1@29351]  com.plasticene.fast.controller.TestController printApiLog$original$kjC7ivXx : api log打印啦....
[1719387065344552960] [INFO ] [2023-11-01 00:13:17.087] [http-nio-18888-exec-1@29351]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : Response result:  {"createTime":null,"updateTime":null,"creator":null,"updater":null,"id":123,"username":null,"password":null,"name":"she哈哈","mobile":"1234556457","gender":1,"status":null,"email":"shepherd@163.com","remark":"测试打印api日志啦"}
[1719387065344552960] [INFO ] [2023-11-01 00:13:17.090] [http-nio-18888-exec-1@29351]  com.plasticene.boot.web.core.aop.ApiLogPrintAspect timeAround : time cost:  1115

可以看到对接口入参和出参都进行详细的打印,但是发现接口参数日志和业务的日志耦合在一起,不太好区分。所以我们就想着把接口参数日志单独打印输出到一个日志文件中,方便查询。其实只需要把上面切面类的@Slf4j指定一个topic

@Slf4j(topic = "ptc.api.log")

然后再日志配置文件进行关联部分配置:

    <!-- 2.5 interface接口日志,时间滚动输出  -->
    <appender name="INTERFACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/app-interface.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/app-interface-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    

    <logger name="ptc.api.log" level="info" additivity="false">
      <Appender-ref ref="CONSOLE"/>
      <appender-ref ref="INTERFACE_FILE"/>
    </logger>

    

需要完整的logback.xml日志文件请留言,我单独私发,这里碍于篇幅问题就不全部展示了。

再次执行上面测试接口示例,日志文件收集如下:接口参数打印的日志单独收集到了app-interface文件中

而我们接口逻辑中打印的日志输出到了app-info中:

3.总结

以上全部就是我们对打印接口参数日志的功能实现方案分析总结,合理打印输出日志有助于提高排查问题的效率,但是也不能随心所欲地打印日志,会对接口性能造成一定影响和分布式日志收集同步延时等。同时我们也分析如何实现动态灵活性,做到可插拔,最后对日志进行分类归档,方便查询。

最后附上切面的代码路径:https://github.com/plasticene/plasticene-boot-starter-parent/blob/main/plasticene-boot-starter-web/src/main/java/com/plasticene/boot/web/core/aop/ApiLogPrintAspect.java

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

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

相关文章

多人游戏小程序源码系统 带完整的搭建教程

现如今&#xff0c;人们对于娱乐和社交的需求也在不断提高。多人游戏作为一种能够满足人们社交需求的游戏类型&#xff0c;越来越受到用户的欢迎。同时&#xff0c;小程序作为一种轻量级的应用程序&#xff0c;在微信等平台上得到了广泛的应用和推广。因此&#xff0c;开发多人…

【LeetCode力扣】287.寻找重复数(中等)

1、题目介绍 原题链接&#xff1a;287. 寻找重复数 - 力扣&#xff08;LeetCode&#xff09; 示例 1&#xff1a; 输入&#xff1a;nums [1,3,4,2,2] 输出&#xff1a;2示例 2&#xff1a; 输入&#xff1a;nums [3,1,3,4,2] 输出&#xff1a;3提示&#xff1a; 1 < n &l…

hbase代码报错(以及解决方法)

》》》直接上代码 package HBase_Apiimport org.apache.hadoop.conf.Configuration import org.apache.hadoop.hbase.{HBaseConfiguration, TableName} import org.apache.hadoop.hbase.client.{Connection, ConnectionFactory, Get, Put} import org.apache.hadoop.hbase.ut…

烟台海森大数据——数据驱动材料研发,本土化为安全护航

随着大数据时代的来临&#xff0c;人们的生产和生活&#xff0c;各方面都在发生着深刻的变化。作为与国计民生息息相关的材料行业&#xff0c;也在数据时代迎来了新的机遇与挑战。 新材料是我国重点推进的战略性新兴产业之一&#xff0c;对于支撑整个战略性新兴产业发展&#…

海康Visionmaster-全局脚本:通过全局脚本加载本地图像的方法

通过 VM 的全局脚本加载本地图像的步骤如下所示&#xff1a; 1、打开全局脚本&#xff0c;选择需要的示例 2、在运行函数中插入如下代码 1. public int Process() 2. { 3. //m_operateHandle 二次开发 SDK 操作句柄 4. if (m_operateHandle IntPtr.Zero) 5. { return ImvsSd…

Solidity在函数里面返回中文报错原因

错误 from solidity: ParserError: Invalid character in string. If you are trying to use Unicode characters, use a unicode"…" string literal. –> contracts/text.sol:90:24: | 90 | return (1,true,“你好”); 原因与解决方法 在Solidity中&#xff0c;…

unittest 统计测试执行case总数,成功数量,失败数量,输出至文件,生成一个简易的html报告带饼图

这是一个Python的单元测试框架的示例代码&#xff0c;主要用于执行测试用例并生成测试报告。其中&#xff0c;通过unittest模块创建主测试类MainTestCase&#xff0c;并加载其他文件中的测试用例&#xff0c;统计用例的执行结果并将结果写入文件&#xff0c;最后生成一个简单的…

应用开发平台集成表单设计器系列之1——技术预研与技术选型

背景 表单的可视化配置&#xff0c;是低代码开发平台的重要组成部分。平台已实现的低代码配置部分&#xff0c;可以配置生成前端vue页面&#xff0c;对于属性较少的实体&#xff0c;如系统基础数据的管理、配置数据的维护&#xff0c;采用标准化的模板模式来生成&#xff0c;配…

腾讯云24元香港服务器有用过的吗?性能如何?

香港云服务器可以选择腾讯云香港地域的轻量应用服务器&#xff0c;轻量2核2G配置、20M峰值带宽、40G SSD系统盘&#xff0c;优惠价格24元一个月&#xff0c;Linux系统是288元一年&#xff0c;Windows系统是360元一年&#xff0c;腾讯云百科txybk.com分享腾讯云香港轻量应用服务…

墨者学院 Ruby On Rails漏洞复现第一题(CVE-2018-3760)

打开 web 页面&#xff1a; 发现是Ruby&#xff0c;在Ruby 3.7.1和更低版本中&#xff0c;存在由辅助解码引起的路径遍历漏洞。攻击者可以使用%252e%252e/访问根目录并读取或执行目标服务器上的任何文件。可以先检测一下是否有此漏洞&#xff1a; /assets/file:%2f%2f/etc/pas…

Cassandra介绍(一)

1.1. 概念 Apache Cassandra 是高度可扩展的&#xff0c;高性能的分布式 NoSQL 数据库。 Cassandra 旨在处理许 多商品服务器上的大量数据&#xff0c;提供高可用性而无需担心单点故障。 Cassandra 具有能够处理大量数据的分布式架构。 数据放置在具有多个复制因子的不同机器…

学习c++的第十三天

目录 文件和流 打开文件 关闭文件 写入文件 读取文件 读取 & 写入实例 文件位置指针 异常处理 扩展知识 抛出异常 标准的异常 定义新的异常 文件和流 到目前为止&#xff0c;我们已经使用了 iostream 标准库&#xff0c;它提供了 cin 和 cout 方法分别用于从标…

一文搞懂优先队列及相关算法

大家好&#xff0c;我是 方圆。优先队列在 Java 中的定义是 PriorityQueue&#xff0c;它基于 二叉堆 数据结构实现&#xff0c;其中的元素并不是全部有序&#xff0c;但它能够支持高效地 获取或删除最值元素。 二叉堆是一种特定条件的 完全二叉树&#xff0c;树的根节点为堆顶…

vivo 网络端口安全建设技术实践

作者&#xff1a;vivo 互联网安全团队 - Peng Qiankun 随着互联网业务的快速发展&#xff0c;网络攻击的频率和威胁性也在不断增加&#xff0c;端口是应用通信中的门户&#xff0c;它是数据进出应用的必经之路&#xff0c;因此端口安全也逐渐成为了企业内网的重要防线之一&…

公司团建小游戏开发小程序游戏互动小游戏

在现代工作环境中&#xff0c;团队合作和员工士气是取得成功的关键因素。为了增强团队合作、提升员工士气&#xff0c;并促进员工之间的互动&#xff0c;公司团建小游戏成为了一种备受欢迎的方式。本文将探讨如何开发公司团建小游戏&#xff0c;以达到这些目标。 1. 游戏概念 …

2023下半年软考信息系统项目管理师上午真题及答案

1.( B )不属于项目建议书的核心内容。 A.项目的必要性 B.初步可行性研究 C.项目的市场预测 D.项目建设必需的条件 解析&#xff1a; 2.在监控项目工作过程中&#xff0c;当遇到变更请求时&#xff0c;为使项目工作绩效重新与项目管理计划致&#xff0c;而进行的有目的的活动…

【双指针+简化去重操作】Leetcode 15 三数之和

【双指针简化操作】Leetcode 15 三数之和 解法1 解法1 新建一个嵌套列表&#xff1a;List<List<Integer>> result new List<>(); 初始化一个ArrayList并直接赋值&#xff1a;ArrayList<Integer> result new ArrayList<>(Arrays.asList(1, 2…

又来安利了,这个Itbuilder在线数据库设计工具用起来太顺手了

对于测试、开发、DBA、运维来说&#xff0c;数据库是再熟悉不过了。 我们都知道如今的数据是多么复杂和难以管理&#xff0c;但幸运的是有数据库设计工具可以帮助我们&#xff0c;可以在市场上找到很多的数据库设计工具&#xff0c;包括itbuilder。这些数据库设计工具可以帮助我…

如何选择SVM中最佳的【核函数】

参数“kernel"在sklearn中可选以下几种 选项&#xff1a; 接下来我们 就通过一个例子&#xff0c;来探索一下不同数据集上核函数的表现。我们现在有一系列线性或非线性可分的数据&#xff0c;我们希望通过绘制SVC在不同核函数下的决策边界并计算SVC在不同核函数下分类准确…

如何对ppt文件设置修改权限?

PPT文件会应用在会议、演讲、课件等工作生活中&#xff0c;当我们制作好了PPT之后&#xff0c;保护内容防止在演示时出错是很重要的&#xff0c;那么如何将PPT文件设置成禁止修改模式呢&#xff1f;今天分享几个方法给大家。 方法一 将PPT文件直接保存或者另存为一份文件&…