如何读取并复制HTTP servlet响应输出流内容以进行日志记录

60
我已经创建了一个过滤器(filter)在我的Java Web服务器中(实际上是AppEngine),用于记录传入请求(request)的参数。我还想记录我的Web服务器编写的响应(response)结果。虽然我可以访问响应对象,但不确定如何从中获取实际的字符串/内容响应。
你有什么想法吗?

你是如何编写响应的呢?是使用 response.getWriter().write(yourResponseString) 吗?还是有其他方法?你是否想要记录错误呢?换句话说,当你使用 response.sendError(yourError) 时,你是否想要记录响应日志? - Dave
1
也许这个链接 http://java.sun.com/blueprints/corej2eepatterns/Patterns/InterceptingFilter.html 和这个链接 http://docstore.mik.ua/orelly/xml/jxslt/ch08_04.htm 可以给你一些提示。 - Sergey Benner
@Dave,就像你提到的那样,只需使用response.getWriter().write(yourResponseString)即可输出旧的结果。 - aloo
使用TeeOutputStream同时写入两个输出流:https://dev59.com/SHA75IYBdhLWcg3wipmH#28305057。 - pdorgambide
6个回答

127
你需要创建一个过滤器(Filter),在其中用自定义的HttpServletResponseWrapper实现包装ServletResponse参数,并覆盖getOutputStream()getWriter()方法,使其返回自定义的ServletOutputStream实现,该实现将写入的字节复制到基本抽象OutputStream#write(int b)方法。然后,将包装的自定义HttpServletResponseWrapper传递给FilterChain#doFilter()调用,最后您应该能够在调用之后获取已复制的响应。

换句话说,这个Filter:
@WebFilter("/*")
public class ResponseLogger implements Filter {

    @Override
    public void init(FilterConfig config) throws ServletException {
        // NOOP.
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        if (response.getCharacterEncoding() == null) {
            response.setCharacterEncoding("UTF-8"); // Or whatever default. UTF-8 is good for World Domination.
        }

        HttpServletResponseCopier responseCopier = new HttpServletResponseCopier((HttpServletResponse) response);

        try {
            chain.doFilter(request, responseCopier);
            responseCopier.flushBuffer();
        } finally {
            byte[] copy = responseCopier.getCopy();
            System.out.println(new String(copy, response.getCharacterEncoding())); // Do your logging job here. This is just a basic example.
        }
    }

    @Override
    public void destroy() {
        // NOOP.
    }

}

自定义的HttpServletResponseWrapper

public class HttpServletResponseCopier extends HttpServletResponseWrapper {

    private ServletOutputStream outputStream;
    private PrintWriter writer;
    private ServletOutputStreamCopier copier;

    public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
        super(response);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called on this response.");
        }

        if (outputStream == null) {
            outputStream = getResponse().getOutputStream();
            copier = new ServletOutputStreamCopier(outputStream);
        }

        return copier;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (outputStream != null) {
            throw new IllegalStateException("getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
            writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (writer != null) {
            writer.flush();
        } else if (outputStream != null) {
            copier.flush();
        }
    }

    public byte[] getCopy() {
        if (copier != null) {
            return copier.getCopy();
        } else {
            return new byte[0];
        }
    }

}

自定义的 ServletOutputStream

public class ServletOutputStreamCopier extends ServletOutputStream {

    private OutputStream outputStream;
    private ByteArrayOutputStream copy;

    public ServletOutputStreamCopier(OutputStream outputStream) {
        this.outputStream = outputStream;
        this.copy = new ByteArrayOutputStream(1024);
    }

    @Override
    public void write(int b) throws IOException {
        outputStream.write(b);
        copy.write(b);
    }

    public byte[] getCopy() {
        return copy.toByteArray();
    }

}

14
想知道为什么获取响应体这么复杂。应该像response.getContent()这样简单。肯定有一些充分的理由在背后 :) - antnewbee
1
@ant:它会占用大量内存,通常对于Web应用程序本身并不感兴趣。 - BalusC
1
@ant:只需设置请求属性即可。 - BalusC
@BalusC:这是可能的吗?我在这里发布了我的问题http://stackoverflow.com/questions/14744442/setting-request-response-attributes-in-spring-rest-environment - antnewbee
7
针对Spring框架,从版本4.1.3开始,也提供了ContentCachingResponseWrapper。该功能可用于对响应内容进行缓存处理。 - Benny Bottema
显示剩余10条评论

15

BalusC的解决方案还可以,但有点过时。现在Spring已经有相关功能了。你只需要使用[ContentCachingResponseWrapper]即可,它有一个方法public byte[] getContentAsByteArray()

我建议创建一个WrapperFactory,允许配置默认的ResponseWrapper或ContentCachingResponseWrapper。


你怎么“使用”它?从稍微尝试一下来看,似乎是用ContentCachingResponseWrapper替换HttpServletResponseCopier -- 是这样吗? - anon

14

你可以使用ContentCachingResponseWrapper,而不是创建自定义的HttpServletResponseWrapper。它提供了getContentAsByteArray()方法。

public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = servletRequest;
        HttpServletResponse response = servletResponse;
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper =new ContentCachingResponseWrapper(response);
        try {
            super.doFilterInternal(requestWrapper, responseWrapper, filterChain);

        } finally {

            byte[] responseArray=responseWrapper.getContentAsByteArray();
            String responseStr=new String(responseArray,responseWrapper.getCharacterEncoding());
            System.out.println("string"+responseStr);       
            /*It is important to copy cached reponse body back to response stream
            to see response */
            responseWrapper.copyBodyToResponse();

        }

    }

1
BalusC 提供的解决方案对我无效,但这个有效。 - abhishek ringsia
需要使用Spring。 - Bob S

5

虽然BalusC的答案在大多数情况下都有效,但你必须小心flush调用-它提交响应并且不能通过以下过滤器进行其他写入。 我们在Websphere环境中发现了一些非常相似的问题,其中交付的响应仅是部分响应。

根据这个问题,根本不应该调用flush,应该让它在内部被调用。

我通过使用TeeWriter(将流分成两个流)并在“分支流”中使用非缓冲流来解决了刷新问题,以进行日志记录。然后就不需要调用flush

private HttpServletResponse wrapResponseForLogging(HttpServletResponse response, final Writer branchedWriter) {
    return new HttpServletResponseWrapper(response) {
        PrintWriter writer;

        @Override
        public synchronized PrintWriter getWriter() throws IOException {
            if (writer == null) {
                writer = new PrintWriter(new TeeWriter(super.getWriter(), branchedWriter));
            }
            return writer;
        }
    };
}

然后你可以这样使用它:
protected void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
    //...
    StringBuilderWriter branchedWriter = new org.apache.commons.io.output.StringBuilderWriter();
    try {
        chain.doFilter(request, wrapResponseForLogging(response, branchedWriter));
    } finally {
        log.trace("Response: " + branchedWriter);
    }
}

代码为简洁起见而简化。


3

-2
如果你只想要响应的有效载荷作为字符串,我会选择:
final ReadableHttpServletResponse httpResponse = (ReadableHttpServletResponse) response;
final byte[] data = httpResponse.readPayload();
System.out.println(new String(data));

1
ReadableHttpServletResponse来自哪里? - Teshte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接