springmvc统一日志打印request和response内容

news2024/11/18 13:25:32

在web项目中,有不少场景需要统一处理一些和实际业务基本不相关的逻辑,比如rest接口的监控、出入参日志、操作记录、统一异常处理(避免将错误堆栈等信息直接打到web端)。

如果你觉得日志打印rest接口出入参非常简单,直接getParameter()就好了,那么多思考3s继续看吧

打印Request中的内容

Servlet处理请求的时候,会将header、url上的参数,已经解析放到了Request对象中了,所以获取header和url上带的参数就非常容易的,直接通过api就可以拿到了。

但是对于body中的内容,会发现Request中是没有任何直接api可以获取body里的内容的,但是可以发现Request中有个InputStream,body中的内容就是通过这个InputStream来获取到的。

所以,知道了这个就简单了,直接将InputStream中的字节全部读出来,构造成一个Stream打印出日志就好了,完美。

但是如果真的这么做了,会发现有一个问题:Controller中的注释@RequestBody的入参会没有值,这不就gg了么

其本质原因就是不带缓存的InputStream是单向的,简单粗暴理解就是:只能读一次,不能重复读的。你在Filter中已经将InputStream读到了末尾,那么后续spring mvc在处理@RequestBody的时候,拿到的InputStream是空的,当然Controller也就没法处理了。

所以解决方式也是很简单的,就是重写InputStream,提供缓存能力,让springmvc在后续的处理中还能获取到内容就好了。

可以翻看下jdk中带Buffer的InputStream,会发现,虽然支持重复度,是需要自己管理那个读游标的,springmvc处理@RequestBody的时候,并没有这么做,所以直接用jdk中待Buffer的InputStream,也就不可行了。

所以就还剩一个办法:将Requst中InputStream的内容先读出来,缓存下来,然后再重写写入到流中,这样,打印日志的时候就可以从缓存中读取内容,而给到spring mvc后续处理逻辑中的InputStream内容也还是原来的内容了。

public class RequestBodyCachableRequestWrapper extends HttpServletRequestWrapper {
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestBodyCachableRequestWrapper.class);
    private              String bodyContent;

    public RequestBodyCachableRequestWrapper(HttpServletRequest request) {
        super(request);
        initReqestBody(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyContent.getBytes());
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBodyString() {
        return this.body;
    }

    private void initReqestBody(HttpServletRequest request) {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            try {
                inputStream = request.getInputStream();
                if (inputStream != null) {
                    bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
                    String line;
                    while ((line = bufferedReader.readLine()) != null) {
                        stringBuilder.append(line);
                    }
                } else {
                    stringBuilder.append("");
                }
            } finally {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (bufferedReader != null) {
                    bufferedReader.close();
                }
            }
            bodyContent = stringBuilder.toString();
        } catch (Exception e) {
            LogUtils.warn(LOGGER, "拦截获得http接口参数异常" + e);
        }
    }
}

打印response中的内容

因为是输出流,jdk中我们没有任何办法从OutputStream中读取去流程中的任何内容,所以从jdk中是找不到办法的,但是但是想想,OutputStream中输出数据的方式,就只有write()方法,那么是不是说只要重载write()方法,将写入的字节给旁路缓存下来,是不是就直接可以从缓存中获取到内容呢?

所以,基本的思路也就有了,我们只是需要自定义一个OutPutStream,然后重写write()方法,将写进来的内容给缓存下来,然后在拦截器中,就能够获取到缓存到自定义OutputStream中的内容了

缓存的方式其实可以直接用个字节数字来缓存,也可以使用一个并行的流,这样,我们就可以不动Response中默认的OutputStream,所以方法就是自定义Response,但是在Response中获取OutputStream的时候,返回的OutPutStream重载一下write()方法,同时写入到这个并行的分支流上,然后我们从这个分支流中获取对应的数据,当然这个分支流就必须要有缓存数据的能力,jdk中的ByteArrayOutputStream其实就是将流中的数据直接写入到自己的缓存字节数组中,那么就可以直接用它来做这个并行的分支流。

public class ResponseBodyCachableResponseWrapper extends HttpServletResponseWrapper {
    private static final Logger LOGGER = LoggerFactory.getLogger(ResponseBodyCachableResponseWrapper.class);

    //旁路输出流,response在将内容通过outputsteam的同时,将内容也写入到这个旁路outputStream,然后打印日志的时候可以从这个旁路outputStream中获取内容
    // 这个旁路输出流的生命期和response中的outputStream同步
    private final ByteArrayOutputStream branchByteArrayOutputStream = new ByteArrayOutputStream();

    public ResponseBodyCachableResponseWrapper(HttpServletResponse response) {
        super(response);
    }

    @Override
    public ServletResponse getResponse() {
        return this;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        // 相当于重载response中保留的outputstream,处理将内容写给前端,同时将内容写给旁路的outputstream,然后旁路outputStream使用带有缓存的outputsream,
        // 打印日志的时候从旁路outputstream中获取值,
        return new ServletOutputStream() {
            
            // 注意这里入参int的含义,这个入参含义有点绕的
            @Override
            public void write(int bytes) throws IOException {
                ResponseBodyCachableResponseWrapper.super.getOutputStream().write(bytes);
                ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.write(bytes);
            }

            @Override
            public void write(byte[] bytes, int off, int len) throws IOException {
                ResponseBodyCachableResponseWrapper.super.getOutputStream().write(bytes, off, len);
                try {
                    ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.write(bytes, off, len);
                } catch (Exception e) {
                    LOGGER.error("write(byte[],off,len)写入分支outputStream失败");
                }
            }

            @Override
            public void write(byte[] bytes) throws IOException {
                ResponseBodyCachableResponseWrapper.super.getOutputStream().write(bytes);
                try {
                    ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.write(bytes);
                } catch (Exception e) {
                    LOGGER.error("write(byte[])写入分支outputStream失败");
                }
            }

            @Override
            public void flush() throws IOException {
                ResponseBodyCachableResponseWrapper.super.getOutputStream().flush();

                try {
                    ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.flush();
                } catch (Exception e) {
                    LOGGER.error("close分支outputStream失败");
                }
            }

            @Override
            public void close() throws IOException {
                ResponseBodyCachableResponseWrapper.super.getOutputStream().close();
                try {
                    ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.close();
                } catch (Exception e) {
                    LOGGER.error("flush分支outputStream失败");
                }
            }
        };
    }

    public byte[] getByteArray() {
        try {
            return this.branchByteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            return new byte[0];
        }
    }
}

关于OutputStream.write(int byte)的理解:

猜测一下再从文件中读出来内容是啥:

  1. 97

  1. a

答案是a。

原因就是a的ascii码是97。write(int byte)其实就是项输出流中写入了4个字节,即将这个int型数据转换成字节后,写入。而在读取的时候,将这个字节当成ascii来解释的,所以答案就是a。

如果我们将这4个字节按照int类型来解释,那就是97。这么解释后可以理解为啥入参的名称叫bytes了吧

不想管那么多的,就想拿来就用的,问题也好办,我找到了一个大兄弟封装好了放到了github上,可以直接干下来就用,它的处理方式是一样的。

https://github.com/isrsal/spring-mvc-logger/blob/master/README.md

有了这两个,那么Filter就好写了,脑补一下就好了。

只是需要特别注意一下,多了这些操作是有成本的,另外就是那种非文本的请求,比如文件/图片/视频/音频等这些的上传下载,是需要排除的,不要用这个封装。

统一异常处理

在spring项目中,不要意思说到统一,就去自定义各种拦截器,然后写一堆aspectj表达式去拉结类。在使用spring mvc的web项目中,除了spring framework,不要忘了还有servlet和spring mvc的扩展点可用,以及广为流传的注解可以帮助来完成很多和业务不相关的统一逻辑处理的。

  1. @RestControllerAdvice+@ExceptionHandler(Exception.class)注解可实现web的统一异常处理

  1. 实现HandlerExceptionResolver接口

但要注意@RestControllerAdvice不单单是统一异常处理的,还可以完成其他事情的

@RestControllerAdvice是一个组合注解,由@ControllerAdvice、@ResponseBody组成,而@ControllerAdvice继承了@Component,因此@RestControllerAdvice本质上是个Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。从而将对于控制器的全局配置放在同一个位置

  • @ExceptionHandler:用于指定异常处理方法,用于全局处理控制器里的异常。

  • @InitBinder:用来设置WebDataBinder,用于自动绑定前台请求参数到Model中。

  • @ModelAttribute:本来作用是绑定键值对到Model中,当与@ControllerAdvice配合使用时,可以让全局的@RequestMapping都能获得在此处设置的键值对

具体使用其实都是非常简单的,随便百度都有示例,只是注意的是统一异常处理本质还是在拦截异常,如果在统一异常处理之前,就将异常给吞掉了,那毫无疑问,就走不到这里的统一异常处理了。

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

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

相关文章

Redis6学习笔记【part4】Jedis-API与手机验证码功能实现

1.连接 Jedis 第一步&#xff0c;修改 redis 的配置&#xff0c;以允许外网 ip 访问 redis。 在 redis.conf 中注释掉 bind 127.0.0.1 &#xff0c;并修改 protected-mode no 。 第二步&#xff0c;导入依赖。 <dependency><groupId>redis.clients</groupId…

Unity 进阶 之 资源文件夹下资源名的重名检查,并简单生产资源表的方法整理

Unity 进阶 之 资源文件夹下资源名的重名检查,并简单生产资源表的方法整理 目录 Unity 进阶 之 资源文件夹下资源名的重名检查,并简单生产资源表的方法整理 一、简单介绍 二、简单实现过程 三、关键代码 一、简单介绍 Unity中的一些知识点整理。 本节简单介绍在Unity开发…

python使用sentinelsat库下载sentinel影像数据

GIS遥感不分家&#xff0c;最近开始找一些影像的下载脚本了&#xff0c;这两天搞定了哨兵和modis的&#xff0c;分别贴一下 鉴于《Python中使用sentinelsat包自动下载Sentinel系列数据》这篇文章已经写得非常全乎&#xff0c;这里就简单补充一下&#xff0c;放个最简单的下载脚…

Vue CLI(Vue.js 开发的标准工具)

Vue CLI&#xff08;Vue.js 开发的标准工具&#xff09;参考描述Vue CLI获取检测项目创建项目Please pick a presetCheck the features needed for your projectChoose a version of Vue.jsPrefer placing configSave this as a preset for future projects?Save preset asFin…

[MRCTF2020]Ezaudit(随机数的安全)

目录 信息收集 代码审计 相关函数 前提知识 思路分析 补充知识 信息收集 查看源代码没有发现有用信息&#xff0c;尝试dirmap扫下目录 python3 dirmap.py -i 网址 -lcf 扫描时发现一个www.zip目录 下载到一份index.php文件&#xff0c;找到一个login.html <?php h…

docker安装pg数据库及pg数据库基本操作

一、首先准备pg数据库的docker镜像二、先创建一个文件作为pg数据库数据文件、配置文件等的外部挂载文件三、创建镜像docker run -it -d --name postgres14 --restartalways --privilegedtrue -p 5432:5432 -e POSTGRES_PASSWORDpostgres -v /home/fengyang/pg_data:/var/lib/po…

SpringBoot+VUE前后端分离项目学习笔记 - 【25 SpringBoot实现1对1、1对多、多对多关联查询】

新增课程Course页面&#xff0c;实现学生选课功能、课程教授老师选择等功能 1. 课程与授课老师是一对一关系 因为course表仅记录了teacherid&#xff0c;而页面需要的是老师的名字 select course.*,sys_user.id from course left join sys_user **on ** course.teacher_id sys…

第六章SpringFramework之声明事务

文章目录JdbcTemplate准备工作导入依赖创建jdbc.properties配置Spring的配置文件配置测试类的环境实例声明式事务概念先看看对应的编程式事务声明式事务通过一个案例了解声明式事务前提准备三层架构的构建模拟场景的情况添加事务Spring声明式事务的属性事务注解标识的位置事务属…

手把手教你学51单片机-点亮你的LED

单片机内部资源 Flash——程序存储空间。对于单片机来说 Flash 最大的意义是断电后数据 不丢失。 RAM——数据存储空间。RAM 是单片机的数据存储空间,用来存储程序运行过程中产生的和需要的数据,关电后数据丢失 SFR——特殊功能寄存器。通过对特殊工程寄存器读写操作,可以…

循环语句(循环结构)——“C”

各位CSDN的uu们好呀&#xff0c;我又来啦&#xff0c;今天&#xff0c;小雅兰给大家介绍的是一个知识点&#xff0c;就是循环语句啦&#xff0c;包括while循环、do-while循环、for循环&#xff0c;话不多说&#xff0c;让我们一起进入循环结构的世界吧 首先&#xff0c;我们先来…

利用Python暴力破解邻居家WiFi密码

如觉得博主文章写的不错或对你有所帮助的话&#xff0c;还望大家多多支持呀&#xff01;关注、点赞、收藏、评论。 文章目录一、编写代码二、展示测试结果三、测试四、生成密码本&#xff08;建议自己找一个密码本&#xff09;一、编写代码 在桌面新建一个文件 如果你新建的文…

如何实现everything的http外网访问

Everything是voidtools开发的一款文件搜索工具&#xff0c;官网描述为“基于名称快速定位文件和文件夹。”可以实现快速文件索引、快速搜索、最小资源使用、便于文件分享等功能。 everything部署本地后&#xff0c;可以开启配置Http访问功能&#xff0c;这样在局域网内就可以直…

【自用】Git日常开发教程

因为经常容易忘记指令&#xff08;年纪大了&#xff09;&#xff0c;所以打算记录一下将一堆文件从vscode上传到GitHub仓库 目录软件下载初始化状态过程可能出现的错误其他操作参考资料软件下载 https://gitforwindows.org/ https://code.visualstudio.com/ 初始化状态 过程 …

上海亚商投顾:两市缩量微涨,数字经济概念全线走强

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪三大指数今日缩量震荡&#xff0c;黄白二线有所分化&#xff0c;题材概念表现活跃。数字经济概念全线走强&#xff0…

8、MariaDB11数据库安装初始化密码

MariaDB11安装 安装前准备 下载安装包 点我去MariaDB官网下载安装包 查看相关文档 Mariadb Server官方文档 使用zip安装 解压缩zip 将下载到的zip解压缩到想安装的位置。 生成data目录 打开cmd并进入到刚才解压后的bin目录&#xff0c; 执行mysql_install_db.exe程序生…

Flowable进阶学习(二)流程部署的深入解析

文章目录一、流程部署涉及表及其结构1. 部署流程代码示例&#xff1a;2. 流程部署所涉及表&#xff1a;3. 流程部署涉及表的结构、字段解析二、流程部署中数据的存储的过程一、流程部署涉及表及其结构 1. 部署流程代码示例&#xff1a; 设计俩个流程&#xff0c;并压缩成zip包…

项目引入多类数据源依赖,MyBatisPlus 是如何确定使用哪种数据源的?

背景 壬寅年腊月廿八&#xff0c;坚守在工作岗位。看了一下项目的 pom.xml 依赖&#xff0c;发现了好几个数据库连接相关的包&#xff0c;有 commons-dbcp2、c3p0、hikaricp、druid-spring-boot-starter&#xff0c;这可是四种不同的数据库连接池呢&#xff0c;一个项目中引入…

Github访问办法

GitHub GitHub是为开发者提供Git仓库的托管服务。这是一个让开发者与朋友、同事、同学及陌生人共享代码的完美场所。 GitHub除提供Git仓库的托管服务外&#xff0c;还为开发者或团队提供了一系列功能&#xff0c;帮助其高效率、高品质地进行代码编写。 GitHub的创始人之一Chr…

使用GPIO模拟I2C的驱动程序分析

使用GPIO模拟I2C的驱动程序分析 文章目录使用GPIO模拟I2C的驱动程序分析参考资料&#xff1a;一、回顾I2C协议1.1 硬件连接1.3 协议细节二、 使用GPIO模拟I2C的要点三、 驱动程序分析3.1 平台总线设备驱动模型3.2 设备树3.3 驱动程序分析1. I2C-GPIO驱动层次2. 传输函数分析四、…

SQL注入简介与原理

数据来源 本文仅用于信息安全学习&#xff0c;请遵守相关法律法规&#xff0c;严禁用于非法途径。若观众因此作出任何危害网络安全的行为&#xff0c;后果自负&#xff0c;与本人无关。 SQL注入 首先从SQL注入存在的代码来看 假如这里的id没有过滤&#xff0c;我们就可以输入…