You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring中记录完整请求响应体如何避免内存占用过高?

解决方案:Spring请求响应全量记录避免OOM问题

核心问题梳理

  • 当设置setMaxPayloadLength(Integer.MAX_VALUE)时,ContentCachingRequestWrapper会直接初始化超大容量的ByteArrayOutputStream,大量请求(尤其是大请求体场景)会瞬间占用巨量内存,触发OOM。
  • 无参构造的ContentCachingRequestWrapper存在设计缺陷:未考虑查询参数会写入缓存,导致请求体+查询参数或纯查询参数超过1024字节时缓存溢出,无法正常记录内容。

可行替代方案

方案1:自定义动态扩容的请求包装类

不修改Spring原生实现,自己实现按需扩容的请求包装类,避免预分配超大内存,同时设置合理的单请求缓存上限(比如10MB):

public class SmartContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private final ByteArrayOutputStream cachedContent = new ByteArrayOutputStream();
    private final int maxCacheSize;
    private boolean isCached = false;

    public SmartContentCachingRequestWrapper(HttpServletRequest request, int maxCacheSize) {
        super(request);
        this.maxCacheSize = maxCacheSize;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (isCached) {
            return new ByteArrayServletInputStream(cachedContent.toByteArray());
        }
        ServletInputStream originalStream = super.getInputStream();
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                int data = originalStream.read();
                if (data != -1 && cachedContent.size() < maxCacheSize) {
                    cachedContent.write(data);
                }
                return data;
            }

            @Override
            public boolean isFinished() {
                return originalStream.isFinished();
            }

            @Override
            public boolean isReady() {
                return originalStream.isReady();
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                originalStream.setReadListener(readListener);
            }
        };
    }

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

复制AbstractRequestLoggingFilter的代码,将其中的ContentCachingRequestWrapper替换为自定义类,同时设置合理的maxCacheSize,既满足大请求记录需求,又不会预分配超大内存。

方案2:用磁盘缓存替代内存缓存

如果必须支持超大请求体(比如超过100MB),可以将缓存内容写入临时文件,避免内存占用:

public class DiskContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private final File tempFile;
    private final OutputStream tempOut;
    private final int maxCacheSize;
    private boolean isCached = false;

    public DiskContentCachingRequestWrapper(HttpServletRequest request, int maxCacheSize) throws IOException {
        super(request);
        this.maxCacheSize = maxCacheSize;
        this.tempFile = File.createTempFile("req-cache-", ".tmp");
        this.tempOut = new BufferedOutputStream(new FileOutputStream(tempFile));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (isCached) {
            return new FileServletInputStream(new FileInputStream(tempFile));
        }
        ServletInputStream originalStream = super.getInputStream();
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                int data = originalStream.read();
                if (data != -1 && tempFile.length() < maxCacheSize) {
                    tempOut.write(data);
                }
                return data;
            }

            @Override
            public boolean isFinished() {
                return originalStream.isFinished();
            }

            @Override
            public boolean isReady() {
                return originalStream.isReady();
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                originalStream.setReadListener(readListener);
            }
        };
    }

    public byte[] getCachedContent() throws IOException {
        tempOut.flush();
        return Files.readAllBytes(tempFile.toPath());
    }

    public void cleanup() {
        tempFile.delete();
    }
}

注意在Filter的afterRequest阶段调用cleanup()删除临时文件,避免磁盘空间被占用。

方案3:分级设置缓存大小

对请求进行分类处理,兼顾性能和内存安全:

  • 常规小请求(比如<1MB)使用原生ContentCachingRequestWrapper,设置小缓存上限;
  • 大请求(通过Content-Length预判)使用自定义动态扩容包装类,或根据业务允许跳过记录。

示例代码:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    boolean isLargeRequest = false;
    String contentLengthHeader = request.getHeader("Content-Length");
    if (contentLengthHeader != null) {
        long contentLength = Long.parseLong(contentLengthHeader);
        isLargeRequest = contentLength > 1024 * 1024; // 1MB阈值
    }

    HttpServletRequest wrappedRequest = request;
    if (isLargeRequest) {
        wrappedRequest = new SmartContentCachingRequestWrapper(request, 10 * 1024 * 1024); // 大请求用10MB上限
    } else {
        wrappedRequest = new ContentCachingRequestWrapper(request, 1024 * 1024); // 小请求用1MB上限
    }

    HttpServletResponse wrappedResponse = new ContentCachingResponseWrapper(response);

    try {
        filterChain.doFilter(wrappedRequest, wrappedResponse);
    } finally {
        // 记录请求内容
        if (wrappedRequest instanceof SmartContentCachingRequestWrapper) {
            byte[] reqContent = ((SmartContentCachingRequestWrapper) wrappedRequest).getCachedContent();
            // 执行记录逻辑
        } else if (wrappedRequest instanceof ContentCachingRequestWrapper) {
            byte[] reqContent = ((ContentCachingRequestWrapper) wrappedRequest).getContentAsByteArray();
            // 执行记录逻辑
        }

        // 记录响应内容
        byte[] resContent = ((ContentCachingResponseWrapper) wrappedResponse).getContentAsByteArray();
        // 执行记录逻辑
        ((ContentCachingResponseWrapper) wrappedResponse).copyBodyToResponse();
    }
}

响应缓存优化补充

原生ContentCachingResponseWrapper同样存在内存预分配问题,可参照上述思路自定义SmartContentCachingResponseWrapper,实现动态扩容或磁盘缓存。


内容的提问来源于stack exchange,提问作者Martin Mucha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 09:42:49