Java Web中HttpServletResponseWrapper捕获静态资源响应长度失效问题
嘿,这个问题我之前做响应统计的时候也踩过坑!核心原因是Tomcat处理静态资源时会用sendFile机制绕开标准的getOutputStream()/getWriter()方法,直接把文件内容发送给客户端,所以你的HttpServletResponseWrapper根本没机会统计这些字节。另外,Tomcat处理静态资源时通常会直接设置Content-Length响应头,而不是通过输出流写入来计算长度,这也会让你的统计逻辑失效。
解决思路:完善你的ResponseWrapper实现
要覆盖所有可能的静态资源处理场景,你需要在HttpServletResponseWrapper里补充几个关键方法的重写,下面是完整的实现方案:
1. 自定义Wrapper覆盖所有统计入口
这个Wrapper会同时处理三种场景:通过输出流写入的内容、直接设置的Content-Length头、Tomcat的sendFile静态资源发送:
public class ContentLengthResponseWrapper extends HttpServletResponseWrapper { private final ByteArrayOutputStream byteArrayOutputStream; private ServletOutputStream servletOutputStream; private PrintWriter printWriter; // 记录通过setContentLength/setContentLengthLong设置的长度 private long recordedContentLength = -1; public ContentLengthResponseWrapper(HttpServletResponse response) { super(response); this.byteArrayOutputStream = new ByteArrayOutputStream(); } @Override public ServletOutputStream getOutputStream() throws IOException { if (printWriter != null) { throw new IllegalStateException("getWriter() has already been called for this response"); } if (servletOutputStream == null) { servletOutputStream = new CountingServletOutputStream(byteArrayOutputStream); } return servletOutputStream; } @Override public PrintWriter getWriter() throws IOException { if (servletOutputStream != null) { throw new IllegalStateException("getOutputStream() has already been called for this response"); } if (printWriter == null) { printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, getCharacterEncoding())); } return printWriter; } // 获取最终统计的内容长度:优先用直接设置的值,否则用输出流的字节数 public long getCalculatedContentLength() { if (recordedContentLength != -1) { return recordedContentLength; } // 确保字符流的内容都刷到字节流里 if (printWriter != null) { printWriter.flush(); } return byteArrayOutputStream.size(); } // 拦截setContentLength方法,记录设置的长度 @Override public void setContentLength(int len) { this.recordedContentLength = len; super.setContentLength(len); } @Override public void setContentLengthLong(long len) { this.recordedContentLength = len; super.setContentLengthLong(len); } // 重写Tomcat特有的sendFile方法,统计静态文件的大小 @Override public void sendFile(String pathname) throws IOException { File staticFile = new File(pathname); if (staticFile.exists() && staticFile.isFile()) { this.recordedContentLength = staticFile.length(); } // 调用原始的sendFile方法发送文件 super.sendFile(pathname); } // 自定义的统计输出流,负责记录写入的字节数 private static class CountingServletOutputStream extends ServletOutputStream { private final OutputStream delegate; public CountingServletOutputStream(OutputStream delegate) { this.delegate = delegate; } @Override public void write(int b) throws IOException { delegate.write(b); } @Override public void write(byte[] b) throws IOException { delegate.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { delegate.write(b, off, len); } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener writeListener) { // 异步场景如果需要可以补充实现,这里先空着 } } }
2. 在Filter中正确使用Wrapper
修改你的TestFilter,确保拦截所有请求(包括静态资源),并在响应完成后获取统计结果:
public class TestFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 初始化逻辑,比如读取配置参数 } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 包装原始响应 ContentLengthResponseWrapper responseWrapper = new ContentLengthResponseWrapper(response); // 继续执行过滤链,让后续的Servlet/资源处理器处理请求 chain.doFilter(request, responseWrapper); // 确保所有输出都被刷新,避免统计遗漏 if (responseWrapper.getWriter() != null) { responseWrapper.getWriter().flush(); } if (responseWrapper.getOutputStream() != null) { responseWrapper.getOutputStream().flush(); } // 获取统计到的内容长度并处理(比如日志、存储) long contentLength = responseWrapper.getCalculatedContentLength(); System.out.printf("请求路径[%s]的响应Content-Length: %d%n", request.getRequestURI(), contentLength); } @Override public void destroy() { // 销毁逻辑,比如释放资源 } }
3. 关键注意事项
- Filter映射路径:确保你的Filter在
web.xml或注解中映射到/*,这样才能拦截所有请求(包括静态资源)。如果只映射到/或者特定Servlet路径,静态资源请求不会经过这个Filter。 - Tomcat版本适配:
sendFile方法是Tomcat对Servlet规范的扩展,在Tomcat 8.5中是org.apache.catalina.connector.Response的实现方法,上面的代码直接重写是因为HttpServletResponseWrapper会委托给底层的Tomcat Response对象,所以能生效。 - Gzip压缩场景:如果你的Tomcat开启了Gzip压缩,那么实际发送给客户端的字节数会比原始内容长度小。这种情况下,你需要选择是统计原始长度还是压缩后的长度——如果要统计压缩后的,你需要在压缩层之后做统计,或者修改Wrapper来捕获压缩后的输出。
内容的提问来源于stack exchange,提问作者lwwwzh
相关产品推荐
相关产品推荐

