亲测可用,SpringBoot项目打印接口请求信息日志,CommonsRequestLoggingFilter实现方式

2023-11-20

需求背景

  1. 线上项目出现bug时,可以通过接口的请求参数来排查定位问题。
  2. 和业务方battle时,能够证明他是自己操作的问题。

效果图

在这里插入图片描述

在这里插入图片描述

实现思路

  • Spring提供了CommonsRequestLoggingFilter过滤器,该过滤器可以在接口请求前和请求后分别打印日志;
  • 通过继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑;

其他方案对比

  1. aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截。
  2. 拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰。

优缺点分析

缺点

  1. 无法获取返回值,适合一些不关心返回值的场景(如果接口出现异常,此时也没有返回值)。
  2. 对于Payload参数,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream()流,所以必须要先调用@RequstBody,才能取到值(可以去看下ContentCachingRequestWrapper的具体实现)

优点

  1. 实现简单,代码层次逻辑清晰,与业务逻辑解耦。

具体实现

继承CommonsRequestLoggingFilter过滤器,实现部分自己的逻辑

package com.yp.basic.log.filter;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.yp.basic.log.properties.OptLogProperties;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Set;

/**
 * 自定义的请求日志打印过滤器,打印所有接口请求参数、耗时等日志 <br/>
 *
 * 对比其他两种方式:<br/>
 * <ul>
 *     <li>1、aop的方式:必须要到controller层,才会打印, 有些接口在 过滤器、拦截器层被拦截</li>
 *     <li>2、拦截器的方式:request.getInputStream()只能调用一次,解决的方法为copy一份流,个人感觉在代码逻辑层面不太清晰</li>
 * </ul>
 *
 *  缺点:<br/>
 *  <ul>
 *      <li>1、无法获取返回值,适合一些不关心返回值的场景。</li>
 *      <li>2. 内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。</li>
 *  </ul>
 *
 *  优点:<br/>
 *  <ul>
 *      <li>1、 实现简单,代码层次逻辑清晰。</li>
 *  </ul>
 *
 * @author: wcong
 * @date: 2022/11/25 15:17
 */
@Slf4j
public class RequestLogPrintFilter extends CommonsRequestLoggingFilter {

    /**
     * 接口请求 开始 日志前缀标识,目的是为了提高每次判断时的性能
     */
    public final static String REQUEST_START_PREFIX_FLAG = "0";
    /**
     * 接口请求 开始 日志前缀
     */
    private final static String REQUEST_START_PREFIX = "### request start[";
    /**
     * 接口请求 结束 日志前缀标识,目的是为了提高每次判断时的性能
     */
    public final static String REQUEST_END_PREFIX_FLAG = "1";
    /**
     * 接口请求 结束 日志前缀
     */
    private final static String REQUEST_END_PREFIX = "### request end[";

    /**
     * 是否为prod环境
     */
    private static final boolean IS_PROD_EVN;
    /**
     * 不打印接口请求日志的url集合
     */
    private static final Set<String> EXCLUDE_HTTP_LOG_URLS;
    static {
        final String activeProfile = SpringUtil.getActiveProfile();
        final OptLogProperties optLogProperties = SpringUtil.getBean(OptLogProperties.class);
        IS_PROD_EVN = "prod".equals(activeProfile);
        EXCLUDE_HTTP_LOG_URLS = optLogProperties.getExcludeHttpLogUrls();
    }

    /**
     * 重写父类方法:封装打印消息的格式
     */
    @Override
    protected String createMessage(HttpServletRequest request, String prefix, String suffix) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return null;
        }

        final StringBuilder messageInfo = getMessageInfo(request, prefix, suffix);
        // 请求开始还是结束
        if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
            // 请求开始
            MDC.put("logStartTime", String.valueOf(System.currentTimeMillis()));
        } else {
            // 请求结束,记录耗时
            final Long logStartTime = Convert.toLong(MDC.get("logStartTime"), 0L);
            messageInfo.append("\r\n接口耗时: ").append(System.currentTimeMillis() - logStartTime).append("ms");
        }
        return messageInfo.toString();
    }

    /**
     * 重写父类方法:请求前调用逻辑
     */
    @Override
    protected void beforeRequest(HttpServletRequest request, String message) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return;
        }
        doPrintLog(message);
    }

    /**
     * 重写父类方法:请求后调用逻辑
     */
    @Override
    protected void afterRequest(HttpServletRequest request, String message) {
        // 是否为不打印的url
        if (isExcludeLogUrl((request.getRequestURI()))) {
            return;
        }
        doPrintLog(message);
    }
    /**
     * 重写父类方法:是否打印日志
     */
    @Override
    protected boolean shouldLog(HttpServletRequest request) {
        // 父类中的逻辑是:logger.isDebugEnabled()
        return true;
    }

    /**
     * 统一封装打印的日志格式
     *
     * @param request   javax.servlet.http.HttpServletRequest
     * @param prefix    打印前缀
     * @param suffix    打印后缀
     * @return 封装好的日志格式
     */
    private StringBuilder getMessageInfo(HttpServletRequest request, String prefix, String suffix) {
        StringBuilder msg = new StringBuilder();

        // 判断是 请求开始 还是 请求结束
        if (REQUEST_START_PREFIX_FLAG.equals(prefix)) {
            msg.append(REQUEST_START_PREFIX);
        } else {
            msg.append(REQUEST_END_PREFIX);
        }

        msg.append(StrUtil.format("method={}; ", request.getMethod().toLowerCase()));
        msg.append("uri=").append(request.getRequestURI());

        // 是否有传递 查询字符串 信息
        if (isIncludeQueryString()) {
            String queryString = request.getQueryString();
            if (queryString != null) {
                msg.append('?').append(queryString);
            }
        }

        // 是否有传递 payload 信息
        if (isIncludePayload()) {
            String payload = getMessagePayload(request);
            if (payload != null) {
                msg.append("; payload=").append(payload);
            }
        }

        // 是否包含 客户端 信息
        if (isIncludeClientInfo()) {
            String client = request.getRemoteAddr();
            if (StrUtil.isNotBlank(client)) {
                msg.append("; client=").append(client);
            }
            HttpSession session = request.getSession(false);
            if (session != null) {
                msg.append("; session=").append(session.getId());
            }
            String user = request.getRemoteUser();
            if (user != null) {
                msg.append("; user=").append(user);
            }
        }

        msg.append(suffix);
        return msg;
    }

    /**
     * 具体打印的方法
     *
     * @param message   打印的消息
     */
    private void doPrintLog(String message) {
        // 生产环境打印debug级别
        if (IS_PROD_EVN) {
            log.debug(message);
        } else {
            log.info(message);
        }
        // log.info(message);
    }


    private Boolean isExcludeLogUrl(String url) {
        return EXCLUDE_HTTP_LOG_URLS.contains(url);
    }

}

注册过滤器

package com.yp.basic.log;

import com.yp.basic.log.properties.OptLogProperties;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

/**
 * 日志自动配置
 *
 * @author: wcong
 * @date: 2022/11/25 15:17
 */
@EnableAsync
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(OptLogProperties.class)
public class LogAutoConfiguration {

    @Bean
    @ConditionalOnProperty(prefix = OptLogProperties.PREFIX, name = "enable-http-log", havingValue = "true", matchIfMissing = true)
    public FilterRegistrationBean<CommonsRequestLoggingFilter> logFilterRegistration() {
        CommonsRequestLoggingFilter filter = new RequestLogPrintFilter();
        // 是否打印header中的内容,参数很多
        filter.setIncludeHeaders(false);
        // 是否打印查询字符串内容
        filter.setIncludeQueryString(true);
        // 是否打印 payLoad内容,内部使用的是ContentCachingRequestWrapper读取的request.getInputStream(),必须要先调用@RequstBody,才能取到值。
        filter.setIncludePayload(true);
        // 是否打印客户端信息(ip、session、remoteUser)
        filter.setIncludeClientInfo(true);
        // 1024字节(1kb),超出部分截取
        // 在UTF-8编码方案中,一个英文字符占用一个字节,一个汉字字符占用三个字节的空间。
        filter.setMaxPayloadLength(1024);
        // 设置 before request 日志前缀,默认为:Before request [
        filter.setBeforeMessagePrefix(RequestLogPrintFilter.REQUEST_START_PREFIX_FLAG);
        // 设置 before request 日志后缀,默认为:]
        filter.setBeforeMessageSuffix("]");
        // 设置 before request 日志前缀,默认为:After request [
        filter.setAfterMessagePrefix(RequestLogPrintFilter.REQUEST_END_PREFIX_FLAG);
        // 设置 after request 日志后缀,默认为:]
        filter.setAfterMessageSuffix("]");

        FilterRegistrationBean<CommonsRequestLoggingFilter> registration = new FilterRegistrationBean<>(filter);
        registration.addUrlPatterns("/*");
        registration.setOrder(0);
        registration.setName("commonsRequestLoggingFilter");
        return registration;
    }

}

OptLogProperties:在yml中提供配置

package com.yp.basic.log.properties;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashSet;
import java.util.Set;

import static com.yp.basic.log.properties.OptLogProperties.PREFIX;

/**
 * 操作日志配置类
 *
 */
@Data
@NoArgsConstructor
@ConfigurationProperties(prefix = PREFIX)
public class OptLogProperties {
    public static final String PREFIX = "xxx.log";

    /**
     * 是否启用
     */
    private Boolean enabled = true;

    /**
     * 是否打印http接口请求日志
     */
    private Boolean enableHttpLog = true;

    /**
     * 不打印http接口请求日志的url
     */
    private Set<String> excludeHttpLogUrls = new HashSet<>();

}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

亲测可用,SpringBoot项目打印接口请求信息日志,CommonsRequestLoggingFilter实现方式 的相关文章

  • 如何用Java写入OS系统日志?

    Mac OS 有一个名为 Console 的应用程序 其中包含记录的消息 错误和故障 我相信 Windows 中的等效项是事件查看器 我想 Linux 上也有一个 但我不知道它是什么 也不知道它在哪里 是否可以像这样从 Java 输出获取消
  • 正确配置JDK环境变量后仍然找不到java命令

    我在 Windows 虚拟机启动时安装 JDK 使用 cloudinit 用户数据将 PowerShell 脚本传输到 Windows 计算机 然后运行该脚本来安装 JDK softwares Get ItemProperty HKLM S
  • 如何防止在 CXF Web 服务客户端中生成 JAXBElement

    我正在尝试使用 CXF 创建一个 Web 服务客户端来使用 WCF Web 服务 当我使用 wsdl2java 时 它生成具有 JAXBElement 类型而不是 String 的对象 我读到有关使用 jaxb bindings xml 文
  • 在哈希图中存储字符和二进制数

    我正在尝试存储字母到二进制数的映射 这是我的映射 h 001 i 010 k 011 l 100 r 101 s 110 t 111 为此 我创建了一个哈希映射并存储了键值对 我现在想显示给定句子的相应二进制值 这是我的代码 package
  • 以点作为分隔符分割字符串

    我想知道我是否要在一个字符串上分割字符串 正确的方式 我的代码是 String fn filename split return fn 0 我只需要字符串的第一部分 这就是我返回第一项的原因 我问这个是因为我在 API 中注意到 意味着任何
  • 在 Android 中绘制一条带有弯曲边缘的线

    I am using canvas drawLine to draw some line in android but the lines are too sharp but i need a curved edges 这里的 1 是我所拥
  • java中队列的实现

    在 Java 中实现队列是一个非常常见的面试问题 我在网上冲浪 看到了许多实现 他们做了一些奇特的事情 比如实现队列接口和编写自己的addLast and removeFirst 方法 我的问题是我不能使用LinkedList 类并使用其预
  • 动画图像视图

    目前我正在开发一款游戏 这是我的游戏的详细信息 用户应选择正确的图像对象 我希望图像从左到右加速 当他们到达终点时 他们应该再次出现在活动中 这是我正在处理的屏幕截图 我有 5 个图像视图 它们应该会加速 您有此类动画的示例代码吗 非常感谢
  • BigDecimal 的 JPA @Size 注释

    我该如何使用 SizeMySQL 的注释DECIMAL x y 列 我在用着BigDecimal 但是当我尝试包括 Size max它不起作用 这是我的代码 Size max 7 2 Column name weight private B
  • 带有面板的 Java Swing JToolbar:外观和感觉

    我有一个JToolbar其中包含多个JPanels 需要 因为我希望每个都有特定的边界 不幸的是 外观管理器无法识别JPanels属于工具栏和JButtons因此 渲染器与普通按钮一样 即没有工具栏上的特殊鼠标悬停效果 更换JPanels
  • Hystrix是否可以订阅CircuitBreaker开启事件?

    对于单元测试 我希望能够订阅 Hystrix 事件 特别是在断路器打开或关闭时发生事件 我四处寻找示例 似乎解决方法是利用指标流并监视断路器标志 由于 Hystrix 是基于 RxJava 构建的 我认为应该在某个地方有一个事件订阅接口 在
  • JavaFx 中装饰且不可移动的舞台

    我想在 JavaFx 中创建一个装饰舞台 它也将不可移动 我正在从另一个控制器类创建这个阶段 我能够创造和展示舞台 但它是自由移动的 我怎样才能创建这个 非常感谢帮助和建议 我把打开新关卡的方法贴出来 private void addRec
  • 尝试在空对象引用上调用虚拟方法“java.lang.String org.jsoup.nodes.Element.ownText()”

    我正在使用下面的代码来获取版本名称 from 应用商店通过使用 jsoup 我正在获取详细信息 但它引发了一些异常 我的代码是 public class ForceUpdateAsync extends AsyncTask
  • Netty中连接关闭后重新连接的最佳方法是什么

    简单场景 扩展 SimpleChannelUpstreamHandler 的较低级别的类 A 此类是发送消息和接收响应的主力 系统其他部分可以使用顶级类 B 来发送和接收消息 可以模拟同步和异步 此类创建 ClientBootstrap 设
  • Hibernate @OneToMany 注释到底是如何工作的?

    我对 Hibernate 还很陌生 我正在通过教程学习它 我在理解到底如何一对多注释作品 所以我有这两个实体类 Student代表一个学生并且Guide代表指导学生的人 因此 每个学生都与一名向导相关联 但一名向导可以跟随多个学生 我想要一
  • Java 8根据Map属性过滤Map对象列表以删除一些重复项

    Have a List
  • 如何使用maven创建基于spring的可执行jar?

    我有一个基于 Maven 的 Spring WS 客户端项目 我想将其打包为单个 jar 在eclipse中 一切运行正常 当我尝试将其打包为可执行 jar 时 我收到 ClassNotFound 异常 因为 Spring jar 未包含在
  • JPA - 非主键字段上的 @OneToOne 关系不起作用

    我有一个 Spring Data JPA 后端 使用 Hibernate 作为 ORM 实现 这是模型 Person MailConfig id PK uid PK FK Person uid uid Entity
  • BoneCP 和 Derby - 如何正确关闭

    I have BoneCP CONNECTION POOL CONNECTION POOL getConfig setJdbcUrl jdbc derby database shutdown true Connection connecti
  • Java中的媒体播放器库[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在评估用于在 Java 中播放音频 视频的库 它不需要 100 Java Java 与本机库的绑定

随机推荐

  • 逻辑思维训练题

    1 估算你所在城市理发店的数量 https www jianshu com p 1431daad2c62 这种思维方式的核心 就是把你要探索的结果用一个数理公式展示出来 我们用最开始的煎饼摊例子来看怎么算 就要用到 利润 收入 变动成本 而
  • 用于包管理的基本命令APT-GET和APT-CACHE

    这篇文章解释你如何快速学习从命令行使用apt get和apt cache安装 移除 升级和搜索软件包 这篇文章提供一些有用命令 它们将帮助你在基于Debian Ubuntu的系统中处理包管理 apt get是什么 apt get工具是一个强
  • Flutter基础Dart单例的实现

    引言 在日常开发搭建基础框架时 常常用到全局使用的数据类或者工具类 比如日期工具类 地图工具类等 在项目一般使用几种单例类来分享心得 有类实例方式 工厂方式以及类静态方式 实例方式 使用DartPad cn网站来练习dart简单代码片段还是
  • [QT编程系列-40]:QML语言简介

    目录 第1章 简介 第2章 QT QML示例 第三章 QML的步骤 第1章 简介 QML Qt Meta Object Language 是Qt框架中用于构建用户界面的声明性语言 它是一个轻量级的语言 用于描述界面的结构和行为 使开发人员能
  • 樱花动漫中的视频下载分析

    昨天有个朋友问我樱花动漫中的视频怎么下载 那么今天我就写篇文章来专门分析下樱花动漫中的视频是怎么下载的 1 还是打开我们的马赛克视频助手 来分析樱花动漫的数据包 当然你们也没有用其他的抓包工具 不过我习惯了用这个 可以使用它的转到来源功能
  • EPI distortion correction形变矫正, eddy, fieldmap等五种不同方法

    EPI distortion correction形变矫正 1 topup eddy 2 fieldmap eddy 2 1 对mag做去脑壳 2 2 基于去过脑壳的mag 1volume bet nii gz数据 对fieldmap进行预
  • STM32 FLASH操作

    STM32 的闪存模块由 主存储器 信息块和闪存存储器接口寄存器等 3 部分组成 主存储器 该部分用来存放代码和数据常数 如 const 类型的数据 对于大容量产品 其被划分为 256 页 每页 2K 字节 注意 小容量和中容量产品则每页只
  • 解决iframe在ios中无法滚动的bug

    在解决iframe在ios无法滚动的bug中 需要在iframe外面包裹一层div 如下 div class scroll wrapper div 然后设置scroll wrapper的样式 给scroll wrapper添加 webkit
  • camunda 流程引擎如何开始并行任务,且有哪些实现方式?

    认识并行任务 在流程引擎开发中 稍微复杂一点的项目都会遇到并行任务 什么是并行任务呢 就是在完成一个流程节点后 下一步会同时开始多个任务 且任务与任务之间互不影响 这个说起来真的有点绕 刚开始我真不理解 试了好多次才想明白 下面就画2条不同
  • 错误的分页写法及修改

    一 错误的写法 api GetMapping groupList ApiImplicitParams ApiImplicitParam name current value 页码 required true dataType Long da
  • MMdetection学习笔记 第一步安装配置

    安装 之前的安装老是出问题 这里重新仔细进行第三遍 参考了哔哩哔哩的视频教程 mmdetection 教程 使用篇 https www bilibili com video BV1Jb4y1r7ir p 3 share source cop
  • Qt(windows下)捕获异常信息并自动重启

    参考 https blog csdn net x85371169 article details 79267592 目前在弄一个工业上用的软件 需要实现无人值守功能 软件经过两三个星期的debug 已经将绝大部分导致软件死机的bug修复 但
  • 使用burpsuite对web进行账号密码暴力破解

    一 打开环境 1 打开php 2 打开burp suite 3 打开小狐狸 进入127 0 0 1 pikachu 二 Repeater 重发器 1 访问pikachu 多次进行用户名和密码的登录 描述 2 burp suite 中抓包 3
  • set和multiset的用法详解

    一 set文档介绍 1 set是按照一定次序存储元素的容器 2 在set中 元素的value也标识它 value就是key 类型为T 并且每个value必须是唯一的 set中的元素 不能在容器中修改 元素总是const 但是可以从容器中插入
  • 【语义分割】2、Mapillary 数据集简介

    文章目录 一 简介 二 类别 三 标注示例 一 简介 Mapillary Vistas 数据集包含 66 类共 25 000 张高分辨率街景场景的数据 其中有 37 个类是以实例区分的标签 数据总量是 cityscapes 的5倍之多 包括
  • Open3D 基于点云高程制作热力图

    目录 一 概述 二 代码实现 三 结果展示 一 概述 如题 基于点云的高程来制作热力图渲染赋色点云 其结果如下图所示 二 代码实现 import numpy as np import open3d as o3d from matplotli
  • leetcode刷题方法

    leetcode刷题方法 一 范围内的200题 二 刷题步骤 三 算法思路 四 更新 1 12日留 此文章借鉴 陈同学在搬砖 微信公众号的一篇文章 https mp weixin qq com s xr2abGNv8wDZJ qyN4Kew
  • HTML、CSS、JavaScript分别实现什么功能?

    学习Web前端开发基础技术需要掌握 HTML CSS JavaScript 那么这三个都是分别实现什么功能的呢 下面和小编一起来看看吧 一 HTML是网页内容的载体 内容就是网页制作者放在页面上想要让用户浏览的信息 可以包含文字 图片 视频
  • Springboot整合dubb3+nacos作注册中心(基础篇)

    1 首先看下项目结构如下 2 新建父工程springboot dubbo模块 pom文件如下
  • 亲测可用,SpringBoot项目打印接口请求信息日志,CommonsRequestLoggingFilter实现方式

    文章目录 需求背景 效果图 实现思路 其他方案对比 优缺点分析 具体实现 需求背景 线上项目出现bug时 可以通过接口的请求参数来排查定位问题 和业务方battle时 能够证明他是自己操作的问题 效果图 实现思路 Spring提供了Comm