首页 > 解决方案 > 使用 spring 过滤器在 Spring Boot 中记录原始 HTTP 请求和响应

问题描述

我正在尝试使用过滤器(比如说LoggingFilter)在 Spring Boot 中记录请求和响应,并尝试从请求正文中获取一些特定属性以登录 MDC。由于getInputStream请求中的方法只能调用一次,因此需要使用请求和响应包装器。为了做到这一点,spring 分别提供了包装类ContentCachingRequestWrapperContentCachingResponseWrapper请求和响应。但是在使用ContentCachingRequestWrapperin 过滤器时,请求数据仅在filterChain.doInternalFilter调用 in 方法后记录LoggingFilter

为了在之前记录请求filterChain.doInternalFilter,我通过对代码进行一些修改,制作了一个自定义的请求包装类,ContentCachingRequestWrapper现在一切正常。但我想知道这样做是否有任何问题(性能影响、不需要的内存初始化、Java 堆内存在某些时候耗尽等),因为获得一些专家意见总是好的。:)

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.FastByteArrayOutputStream;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;

/**
 * this file is a copy of Spring's ContentCachingRequestWrapper with few modifications.
 */

@Slf4j
public class MDCContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
    private final ByteArrayOutputStream cachedContent;
    private final Integer contentCacheLimit;
    private ServletInputStream inputStream;
    private BufferedReader reader;
    private FastByteArrayOutputStream fastByteArrayOutputStream;

    public String getPayload() {
//        return payload;
        return new String(fastByteArrayOutputStream.toByteArray());
    }

    private String payload;


    public MDCContentCachingRequestWrapper(HttpServletRequest request) {
        super(request);
        int contentLength = request.getContentLength();
        if (isFormPost()) {
            this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
        } else {
            this.cachedContent = new ByteArrayOutputStream();
        }
        this.contentCacheLimit = null;
        try {
            this.getParameterMap();
        } catch (Exception e) {
            log.error("Unable to get parameters map.");
        }

//        StringBuilder stringBuilder = new StringBuilder();
//        try {
//            byte[] buffer = new byte[1024];
//            InputStream inputStream = request.getInputStream();
//            int i = 0;
//            while (-1 != (i = inputStream.read(buffer))) {
//                stringBuilder.append(new String(buffer, 0, i));
//            }
//            inputStream.close();
//        } catch (IOException e) {
//            log.error("can't clone input stream: ", e);
////                throw new RuntimeException(e);
//        }
//        payload = stringBuilder.toString();

        fastByteArrayOutputStream = new FastByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
        try {
            byte[] buffer = new byte[1024];
            InputStream inputStream = request.getInputStream();
            int i = 0;
            while (-1 != (i = inputStream.read(buffer))) {
                fastByteArrayOutputStream.write(buffer, 0, i);
            }
            inputStream.close();
        } catch (IOException e) {
            log.error("can't clone input stream to byteOutputStream: ", e);
//                throw new RuntimeException(e);
      }

    }

    public MDCContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
        super(request);
        this.cachedContent = new ByteArrayOutputStream(contentCacheLimit);
        this.contentCacheLimit = contentCacheLimit;
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
//            InputStream byteArrayInputStream = IOUtils.toInputStream(this.getPayload(), this.getCharacterEncoding());
            InputStream byteArrayInputStream = fastByteArrayOutputStream.getInputStream();
            this.inputStream = new ServletInputStream() {
                boolean overflow = false;

                public int read() throws IOException {
                    int ch = byteArrayInputStream.read();
                    if (ch != -1 && !overflow) {
                        if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
                            overflow = true;
                            handleContentOverflow(contentCacheLimit);
                        } else {
                            cachedContent.write(ch);
                        }
                    } else {
                        byteArrayInputStream.close();
                    }

                    return ch;
                }

                @Override
                public boolean isFinished() {
                    try {
                        return byteArrayInputStream.available() == 0;
                    } catch (IOException e) {
                        return false;
                    }

                }

                @Override
                public boolean isReady() {
                    return true;
                }

                @Override
                public void setReadListener(ReadListener readListener) {
                    log.info("setReadListener method called.");
                }
            };
        }
        return this.inputStream;
    }

    public String getCharacterEncoding() {
        String enc = super.getCharacterEncoding();
        return enc != null ? enc : "ISO-8859-1";
    }

    @Override
    public BufferedReader getReader() throws IOException {
        if (this.reader == null) {
            this.reader = new BufferedReader(new InputStreamReader(this.getInputStream(), this.getCharacterEncoding()));
        }
        return this.reader;
    }

    public String getParameter(String name) {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameter(name);
    }

    public Map<String, String[]> getParameterMap() {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterMap();
    }

    public Enumeration<String> getParameterNames() {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterNames();
    }

    public String[] getParameterValues(String name) {
        if (this.cachedContent.size() == 0 && this.isFormPost()) {
            this.writeRequestParametersToCachedContent();
        }
        return super.getParameterValues(name);
    }

    private boolean isFormPost() {
        String contentType = this.getContentType();
        return contentType != null && contentType.contains("application/x-www-form-urlencoded") && HttpMethod.POST.matches(this.getMethod());
    }

    private void writeRequestParametersToCachedContent() {
        try {
            if (this.cachedContent.size() == 0) {
                String requestEncoding = this.getCharacterEncoding();
                Map<String, String[]> form = super.getParameterMap();
                Map<String, Object> resultMap = new HashMap<>();
                Iterator nameIterator = form.keySet().iterator();

                while (nameIterator.hasNext()) {
                    String name = (String) nameIterator.next();
                    List<String> values = Arrays.asList((String[]) form.get(name));
                    Iterator valueIterator = values.iterator();
                    while (valueIterator.hasNext()) {
                        String value = (String) valueIterator.next();
                        this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes());
                        if (value != null) {
                            this.cachedContent.write(61);
                            this.cachedContent.write(URLEncoder.encode(value, requestEncoding).getBytes());
                            if (valueIterator.hasNext()) {
                                this.cachedContent.write(38);
                            }
                        }
                    }

                    if (nameIterator.hasNext()) {
                        this.cachedContent.write(38);
                    }
                }
            }

        } catch (IOException e) {
            throw new IllegalStateException("Failed to write request parameters to cached content", e);
        }
    }

    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }

    protected void handleContentOverflow(int contentCacheLimit) {
    }

}

另外我想知道使用哪种方法来复制 inputStream 数据:字符串方法或 usingFastByteArrayOutputStream方法,或者我应该使用什么方法。

标签: javaspringspring-bootservlet-filters

解决方案


推荐阅读