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

news/2023/6/7 0:42:48

在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);}@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyContent.getBytes());return new ServletInputStream() {@Overridepublic int read() throws IOException {return byteArrayInputStream.read();}};}@Overridepublic 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);}@Overridepublic ServletResponse getResponse() {return this;}@Overridepublic ServletOutputStream getOutputStream() throws IOException {// 相当于重载response中保留的outputstream,处理将内容写给前端,同时将内容写给旁路的outputstream,然后旁路outputStream使用带有缓存的outputsream,// 打印日志的时候从旁路outputstream中获取值,return new ServletOutputStream() {// 注意这里入参int的含义,这个入参含义有点绕的@Overridepublic void write(int bytes) throws IOException {ResponseBodyCachableResponseWrapper.super.getOutputStream().write(bytes);ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.write(bytes);}@Overridepublic 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失败");}}@Overridepublic 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失败");}}@Overridepublic void flush() throws IOException {ResponseBodyCachableResponseWrapper.super.getOutputStream().flush();try {ResponseBodyCachableResponseWrapper.this.branchByteArrayOutputStream.flush();} catch (Exception e) {LOGGER.error("close分支outputStream失败");}}@Overridepublic 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.exyb.cn/news/show-4566086.html

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32设备驱动-MPU-9250 3轴陀螺仪/加速度计/磁力计驱动

MPU-9250 3轴陀螺仪/加速度计/磁力计驱动 1、MPU9250介绍 MPU-9250 是面向智能手机、平板电脑、可穿戴传感器和其他消费市场的第二代 9 轴运动处理单元™(Motion Processing Unit™)。 MPU-9250 采用 3x3x1mm QFN 封装,是世界上最小的 9 轴运动跟踪设备,采用了最新的 Inven…

转: 云计算openstack—云计算、大数据、人工智能

原文: https://www.cnblogs.com/cloudhere/p/10768850.html 一、互联网行业及云计算 在互联网时代,技术是推动社会发展的驱动,云计算则是一个包罗万象的技术栈集合,通过网络提供IAAS、PAAS、SAAS等资源,涵盖从数据中心底层的硬件…

扫盲:云计算、大数据和人工智能

本文作者凭借其天马行空的脑回路,用最深入浅出,清晰化的文字逻辑,讲明白了云计算、大数据和人工智能三者之间的关系。 故事里面三个角色:兼具经济效益与情怀的云计算;努力把信息变为智慧的大数据;模拟人类…

SpringAMQP - 发布订阅模式

目录 发布订阅介绍 FanoutExchange简介 FanoutExchange案例 常见小问题 DirectExchange简介 DirectExchange案例 常见小问题 TopicExchange简介 TopicExchange案例 发布订阅介绍 发布(Publish)、订阅(Subscribe)发布订阅…

vscode:snippet基本使用方法

ctrlshiftp然后输入snippet, 选择configure user snippets.然后会让你选择语言,我这里选择govscode会建立一个go.json编辑方式就看我下面的示例吧.//输入pln拿到fmt.Println(), "pln":{ //snippet的名称,用来唯一标识snippet"prefix":"pln", //要…

删除VSCode 中自定义的snippets

snippets 在vscode中的自定义一个snippets是一个非常睿智的选择,可以帮我们节省大量的时间。具体操作非常简单,随意百度即可。这边记录一下如何删除的问题。 删除自定义的snippets 当我们新建一个snippets后,它就会一直保存在你的电脑里&a…

Vim 自定义补全利器 Snippet

Vim Snippet 设置 本人是 vim 用户,可以说能不用 IDE 就不用 IDE。 Snippet 是一种支持用户自定义补全的需求,在 vim 中,可以使用 UltiSnips 和 Vim-Snippets 两个插件进行补全。UltiSnips 类似于一个替换引擎,本身是没有任何补全…

vsCode配置用户代码片段

前言:小伙伴们是不是经常看到别人写代码的时候特别迅速,输入几个字母按下enter键就会出现一堆已经写好的代码,那你知道这是怎么实现的吗?接下来可以尝试按照下面的步骤进行配置,会让你写代码事半功倍哟!先演…