文章目录
- 日志输出-第三章-接口级出入参输出完整数据的实现
- 一、概述
- 二、如何输出 Request 的 body
- 2.1、工具类
- 2.2、包装类
- 2.3、如何使用
- 三、如何输出 Response 的 body
- 3.1、包装类
- 3.2、如何使用
日志输出-第三章-接口级出入参输出完整数据的实现
前置内容
- 日志输出指南
- 日志输出-第二章-接口级出入参的实现
一、概述
上一章贴了日志出入参的代码,在第三节的内容中描述了为什么没有输出 body 内容的原因(我个人认为上一章的方案是最优解),本章的教程主要是如何输出 body 中的数据。
正常情况下也是拿来做数据加解密处理之类的
二、如何输出 Request 的 body
这一块的主要问题在于流不可重复读,所以我们通过对 ServletRequest
进行包装的方式,来达到重复读取流的效果。
但是弊端也会很明显,body
的数据会被拷贝多份,一方面内存压力会增大,另一方面性能损耗也不小,所以一般在日志中很少会输出 Request
中 body
的数据。
2.1、工具类
java">package com.lzl.study.scaffold.studyscaffold.common.log;import lombok.extern.slf4j.Slf4j;import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;/*** @ClassName RequestResponseUtil* @Author lizelin* @Description request、response 工具类* @Date 2024/5/27 18:26* @Version 1.0*/
@Slf4j
public abstract class RequestResponseUtil {/*** @Param request* @Return java.lang.String* @Description 获取 Request 中 Body* @Author lizelin* @Date 2023/11/3 16:08**/public static String getRequestBody(HttpServletRequest request) {StringBuilder stringBuilder = new StringBuilder();BufferedReader bufferedReader = null;InputStream inputStream = null;InputStreamReader inputStreamReader = null;try {inputStream = request.getInputStream();if (inputStream != null) {inputStreamReader = new InputStreamReader(inputStream);bufferedReader = new BufferedReader(inputStreamReader);char[] charBuffer = new char[128];int bytesRead;while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {stringBuilder.append(charBuffer, 0, bytesRead);}}} catch (IOException e) {log.error("读取流失败:",e);} finally {close(bufferedReader, inputStream,inputStreamReader);}return stringBuilder.toString();}/*** @Param bufferedReader* @Param inputStream* @Return void* @Description 关闭流* @Author lizelin* @Date 2023/11/3 16:08**/private static void close(BufferedReader bufferedReader, InputStream inputStream,InputStreamReader inputStreamReader) {try {if (inputStream != null) {inputStream.close();}if (bufferedReader != null) {bufferedReader.close();}if (inputStreamReader != null){inputStreamReader.close();}} catch (IOException e) {log.info("流关闭失败", e);}}}
2.2、包装类
java">package com.lzl.study.scaffold.studyscaffold.common.log;import cn.hutool.core.collection.CollUtil;
import org.apache.catalina.util.ParameterMap;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Map;/*** @ClassName LogHttpServletRequestWrapper* @Author lizelin* @Description 日志 Http Servlet 请求包装器* @Date 2024/5/27 18:26* @Version 1.0*/
public class LogHttpServletRequestWrapper extends HttpServletRequestWrapper {/*** 所有参数的 Map 集合*/private Map<String, String[]> parameterMap;/*** 存储 body 数据的容器(这里存储为解析流后的JSON)*/private String body;/*** @Param* @Return java.lang.String* @Description 获取Body* @Author lizelin* @Date 2023/11/3 16:09**/public String getBody() {return this.body;}/*** @Param body* @Return void* @Description 修改 body* @Author lizelin* @Date 2023/11/3 16:09**/public void setBody(String body) {this.body = body;}public LogHttpServletRequestWrapper(HttpServletRequest request) {super(request);// 给参数集合赋值parameterMap = request.getParameterMap();// 获取Bodybody = RequestResponseUtil.getRequestBody(request);}/*** @Param parameterMap* @Return void* @Description 替换整个参数 Map* @Author lizelin* @Date 2023/11/3 14:59**/public void setParameterMap(Map<String, String[]> parameterMap) {this.parameterMap = parameterMap;}/*** @Param key* @Param value* @Return void* @Description 向参数集合中添加参数* @Author lizelin* @Date 2023/11/3 14:59**/public void putParameterMap(String key, String[] value) {if (this.parameterMap instanceof ParameterMap) {((ParameterMap<String, String[]>) this.parameterMap).setLocked(false);}this.parameterMap.put(key, value);}/*** @Param* @Return java.util.Enumeration<java.lang.String>* @Description 获取所有参数名* @Author lizelin* @Date 2023/11/3 14:59**/@Overridepublic Enumeration<String> getParameterNames() {return CollUtil.asEnumeration(parameterMap.keySet().iterator());}/*** @Param name* @Return java.lang.String* @Description 获取指定参数名的值,如果有重复的参数名,则返回第一个的值 接收一般变量 ,如 text 类型* @Author lizelin* @Date 2023/11/3 14:59**/@Overridepublic String getParameter(String name) {ArrayList<String> values = CollUtil.toList(parameterMap.get(name));if (CollUtil.isNotEmpty(values)) {return values.get(0);} else {return null;}}/*** @Param name* @Return java.lang.String[]* @Description 获取单个的某个 key 的 value* @Author lizelin* @Date 2023/11/3 14:58**/@Overridepublic String[] getParameterValues(String name) {return parameterMap.get(name);}/*** @Param* @Return java.util.Map<java.lang.String, java.lang.String [ ]>* @Description 获取值列表* @Author lizelin* @Date 2023/11/3 14:58**/@Overridepublic Map<String, String[]> getParameterMap() {return parameterMap;}/*** @Param* @Return java.lang.String* @Description 获取 queryString* @Author lizelin* @Date 2023/11/3 14:57**/@Overridepublic String getQueryString() {return super.getQueryString();}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(this.getInputStream()));}/*** @Param* @Return javax.servlet.ServletInputStream* @Description 重写获取输入流,因为在输出日志的时候会读取输入流,而流只能读取一次,所以在向后传递的时候就需要做特殊处理* @Author lizelin* @Date 2023/11/3 14:55**/@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {//不需要重写}@Overridepublic int read() {//重写读return byteArrayInputStream.read();}};}
}
2.3、如何使用
做一个转换就完事了
java">/*** @ClassName LogHandlerInterceptor* @Author lizelin* @Description 日志请求入参过滤器* @Date 2023/11/3 12:15* @Version 1.0*/
@Slf4j
@Component
@AllArgsConstructor
public class LogRecordRequestFilter implements Filter, Ordered {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) request;//这里主要是由于流不可重复读取,采用包装类的方式LogHttpServletRequestWrapper requestWrapper = new LogHttpServletRequestWrapper(httpServletRequest);String requestId = requestWrapper.getHeader("requestId");String requestWrapperBody = requestWrapper.getBody();String queryString = requestWrapper.getQueryString();String servletPath = requestWrapper.getServletPath();//输出请求日志log.info("LogRecordRequestFilter HttpServletRequest servletPath:{},requestId:{},queryString:{},body:{}", servletPath, requestId, queryString, requestWrapperBody);chain.doFilter(requestWrapper, response);}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}
}
三、如何输出 Response 的 body
操作和 第二章 的内容差别不大,思路一致。
3.1、包装类
java">package com.lzl.study.scaffold.studyscaffold.common.log;import lombok.extern.slf4j.Slf4j;import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;/*** @ClassName LogHttpServletResponseWrapper* @Author lizelin* @Description 日志 Http Servlet 响应 包装器* @Date 2024/5/27 18:25* @Version 1.0*/
@Slf4j
public class LogHttpServletResponseWrapper extends HttpServletResponseWrapper {private ByteArrayOutputStream byteArrayOutputStream = null;private ServletOutputStream servletOutputStream = null;private PrintWriter printWriter = null;public LogHttpServletResponseWrapper(HttpServletResponse response) throws IOException {super(response);byteArrayOutputStream = new ByteArrayOutputStream();servletOutputStream = new ResponseWrapperOutputStream(byteArrayOutputStream, response);printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, this.getCharacterEncoding()));}/*** @Param* @Return javax.servlet.ServletOutputStream* @Description 获取 OutputStream* @Author lizelin* @Date 2023/11/3 16:47**/@Overridepublic ServletOutputStream getOutputStream() throws IOException {return servletOutputStream;}/*** @Param* @Return java.io.PrintWriter* @Description 获取 Writer* @Author lizelin* @Date 2023/11/3 16:46**/@Overridepublic PrintWriter getWriter() throws UnsupportedEncodingException {return printWriter;}/*** @Param* @Return void* @Description 获取 flushBuffer* @Author lizelin* @Date 2023/11/3 16:46**/@Overridepublic void flushBuffer() throws IOException {if (servletOutputStream != null) {servletOutputStream.flush();}if (printWriter != null) {printWriter.flush();}}/*** @Param* @Return void* @Description 重置流* @Author lizelin* @Date 2023/11/3 16:46**/@Overridepublic void reset() {byteArrayOutputStream.reset();}/*** @Param* @Return String* @Description 读取流中的数据* @Author lizelin* @Date 2023/11/3 16:43**/public String getBody() throws IOException {//刷新缓冲区flushBuffer();//读取流中的数据return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);}/*** @Param* @Return* @Description 内部类,对 ServletOutputStream 进行包装,方便日志记录* @Author lizelin* @Date 2023/11/3 16:43**/private class ResponseWrapperOutputStream extends ServletOutputStream {private ByteArrayOutputStream bos = null;private HttpServletResponse response = null;public ResponseWrapperOutputStream(ByteArrayOutputStream stream, HttpServletResponse response) {bos = stream;this.response = response;}/*** @Param b* @Return void* @Description 将写暂存一份方便日志* @Author lizelin* @Date 2023/11/3 17:31**/@Overridepublic void write(int b) throws IOException {bos.write(b);response.getOutputStream().write(b);}/*** @Param b* @Return void* @Description 将写暂存一份方便日志* @Author lizelin* @Date 2023/11/3 17:32**/@Overridepublic void write(byte[] b) throws IOException {bos.write(b, 0, b.length);response.getOutputStream().write(b, 0, b.length);}@Overridepublic boolean isReady() {return false;}@Overridepublic void setWriteListener(WriteListener writeListener) {//不需要重新}}
}
3.2、如何使用
这个其实也和 2.3 的内容基本一样,我只输出一个简单示例,其他的可以自己往里面添加
java">/*** @ClassName LogHandlerInterceptor* @Author lizelin* @Description 日志请求入参过滤器* @Date 2023/11/3 12:15* @Version 1.0*/
@Slf4j
@Component
@AllArgsConstructor
public class LogRecordRequestFilter implements Filter, Ordered {private final NacosConfig nacosConfig;@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {//热更新//自定义配置 headerHttpServletRequest httpServletRequest = (HttpServletRequest) request;//post put 读//todo 配置读取 body 类型为json//这里主要是由于流不可重复读取,采用包装类的方式LogHttpServletRequestWrapper requestWrapper = new LogHttpServletRequestWrapper(httpServletRequest);String requestId = requestWrapper.getHeader("requestId");String requestWrapperBody = requestWrapper.getBody();String queryString = requestWrapper.getQueryString();String servletPath = requestWrapper.getServletPath();//输出请求日志log.info("LogRecordRequestFilter HttpServletRequest servletPath:{},requestId:{},queryString:{},body:{}", servletPath, requestId, queryString, requestWrapperBody);LogHttpServletResponseWrapper responseWrapper = new LogHttpServletResponseWrapper((HttpServletResponse) response);chain.doFilter(requestWrapper, responseWrapper);//输出响应日志String responseWrapperBody = responseWrapper.getBody();log.info("LogRecordRequestFilter HttpServletResponse servletPath:{},requestId:{},body:{}",servletPath,requestId,responseWrapperBody);}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}
}
如何输出 body 的内容就已经完成了,下一章内容为如何对这些数据进行加解密(单体应用的前后端数据加解密)。