Bootstrap

日志输出-第三章-接口级出入参输出完整数据的实现

在这里插入图片描述

日志输出-第三章-接口级出入参输出完整数据的实现

前置内容

  1. 日志输出指南
  2. 日志输出-第二章-接口级出入参的实现

一、概述

上一章贴了日志出入参的代码,在第三节的内容中描述了为什么没有输出 body 内容的原因(我个人认为上一章的方案是最优解),本章的教程主要是如何输出 body 中的数据。

正常情况下也是拿来做数据加解密处理之类的

二、如何输出 Request 的 body

这一块的主要问题在于流不可重复读,所以我们通过对 ServletRequest 进行包装的方式,来达到重复读取流的效果。

但是弊端也会很明显,body 的数据会被拷贝多份,一方面内存压力会增大,另一方面性能损耗也不小,所以一般在日志中很少会输出 Requestbody 的数据。

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 的内容就已经完成了,下一章内容为如何对这些数据进行加解密(单体应用的前后端数据加解密)。

;