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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 09:16:09