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
相关产品推荐
相关产品推荐

