【问题标题】:How to read and copy the HTTP servlet response output stream content for logging如何读取和复制 HTTP servlet 响应输出流内容以进行日志记录
【发布时间】:2012-02-14 12:36:23
【问题描述】:

我在我的 Java 网络服务器(实际上是 appengine)中创建了一个过滤器,用于记录传入请求的参数。我还想记录我的网络服务器写入的结果响应。虽然我可以访问响应对象,但我不确定如何从中获取实际的字符串/内容响应。

有什么想法吗?

【问题讨论】:

标签: java servlets logging servlet-filters


【解决方案1】:

您需要创建一个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();
    }

}

【讨论】:

  • HttpServletResponseCopier 的构造函数名称不正确,我无法编辑它,因为编辑应该超过 6 个字符,我不想更改答案的任何其他内容。跨度>
  • 想知道为什么获取响应正文如此复杂。它应该类似于 response.getContent()。这背后一定有一些坚实的理由:)
  • @ant:这会占用内存,并且通常对 webapp 本身不感兴趣。
  • @ant:设置一个请求属性即可。
  • 如果是Spring,从4.1.3版本开始,还有ContentCachingResponseWrapper
【解决方案2】:

BalusC 解决方案还可以,但有点过时了。 Spring 现在有它的功能。您需要做的就是使用[ContentCachingResponseWrapper],它有方法public byte[] getContentAsByteArray()

我建议让 WrapperFactory 允许使其可配置,无论是使用默认的 ResponseWrapper 还是 ContentCachingResponseWrapper。

【讨论】:

  • 你如何“使用”它?从玩了一下,看起来你用 ContentCachingResponseWrapper 替换了 HttpServletResponseCopier ——对吗?
【解决方案3】:

而不是创建自定义 HttpServletResponseWrapper。您可以使用 ContentCachingResponseWrapper,因为它提供方法 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();

        }

    }

【讨论】:

  • BalusC 的上述解决方案对我不起作用,但这个解决方案有效
  • 虽然需要春天
【解决方案4】:

虽然BalusC's answer 在大多数情况下都可以工作,但您必须小心flush 调用 - 它会提交响应,并且不可能对其进行其他写入,例如。通过以下过滤器。 我们在 Websphere 环境中发现了一些非常相似的方法存在的问题,其中交付的响应只是部分的。

根据this question,flush 根本不应该被调用,你应该让它在内部被调用。

我通过使用TeeWriter(它将流分成2个流)并在“分支流”中使用非缓冲流来进行日志记录,解决了刷新问题。则无需拨打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);
    }
}

代码被简化了。

【讨论】:

    【解决方案5】:

    我对 appengine 不太熟悉,但您需要 Tomcat 中的 Access Log Valve。它的属性 pattern ;一种格式布局,用于标识要记录的请求和响应中的各种信息字段,或者用于选择标准格式的常用词或组合词。

    看起来 appengine 已经为log filtering 内置了功能。

    apply a servlet filter

    【讨论】:

      【解决方案6】:

      如果您只想将响应负载作为字符串,我会选择:

      final ReadableHttpServletResponse httpResponse = (ReadableHttpServletResponse) response;
      final byte[] data = httpResponse.readPayload();
      System.out.println(new String(data));
      

      【讨论】:

        猜你喜欢
        • 2013-04-04
        • 2017-04-14
        • 1970-01-01
        • 2017-08-07
        • 1970-01-01
        • 1970-01-01
        • 2012-10-20
        • 2013-12-13
        相关资源
        最近更新 更多