文章目录
日志输出-第三章-接口级出入参输出完整数据的实现
前置内容
一、概述
上一章贴了日志出入参的代码,在第三节的内容中描述了为什么没有输出 body 内容的原因(我个人认为上一章的方案是最优解),本章的教程主要是如何输出 body 中的数据。
正常情况下也是拿来做数据加解密处理之类的
二、如何输出 Request 的 body
这一块的主要问题在于流不可重复读,所以我们通过对 ServletRequest
进行包装的方式,来达到重复读取流的效果。
但是弊端也会很明显,body
的数据会被拷贝多份,一方面内存压力会增大,另一方面性能损耗也不小,所以一般在日志中很少会输出 Request
中 body
的数据。
2.1、工具类
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、包装类
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();
// 获取Body
body = 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
**/
@Override
public 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
**/
@Override
public 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
**/
@Override
public 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
**/
@Override
public Map<String, String[]> getParameterMap() {
return parameterMap;
}
/**
* @Param
* @Return java.lang.String
* @Description 获取 queryString
* @Author lizelin
* @Date 2023/11/3 14:57
**/
@Override
public String getQueryString() {
return super.getQueryString();
}
@Override
public 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
**/
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
//不需要重写
}
@Override
public int read() {
//重写读
return byteArrayInputStream.read();
}
};
}
}
2.3、如何使用
做一个转换就完事了
/**
* @ClassName LogHandlerInterceptor
* @Author lizelin
* @Description 日志请求入参过滤器
* @Date 2023/11/3 12:15
* @Version 1.0
*/
@Slf4j
@Component
@AllArgsConstructor
public class LogRecordRequestFilter implements Filter, Ordered {
@Override
public 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);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
三、如何输出 Response 的 body
操作和 第二章 的内容差别不大,思路一致。
3.1、包装类
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
**/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return servletOutputStream;
}
/**
* @Param
* @Return java.io.PrintWriter
* @Description 获取 Writer
* @Author lizelin
* @Date 2023/11/3 16:46
**/
@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
return printWriter;
}
/**
* @Param
* @Return void
* @Description 获取 flushBuffer
* @Author lizelin
* @Date 2023/11/3 16:46
**/
@Override
public 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
**/
@Override
public 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
**/
@Override
public 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
**/
@Override
public void write(byte[] b) throws IOException {
bos.write(b, 0, b.length);
response.getOutputStream().write(b, 0, b.length);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
//不需要重新
}
}
}
3.2、如何使用
这个其实也和 2.3 的内容基本一样,我只输出一个简单示例,其他的可以自己往里面添加
/**
* @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;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//热更新
//自定义配置 header
HttpServletRequest 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);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
如何输出 body 的内容就已经完成了,下一章内容为如何对这些数据进行加解密(单体应用的前后端数据加解密)。