【问题标题】:Logging all network traffic in Spring mvc在 Spring mvc 中记录所有网络流量
【发布时间】:2015-09-05 17:06:20
【问题描述】:

我有使用 RequestBody 和 ResponseBody 注释的 spring mvc 应用程序。它们配置有 MappingJackson2HttpMessageConverter。我也设置了 slf4j。我想记录所有从我的控制器进出的json。 我确实延长了

MappingJackson2HttpMessageConverter

@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {
    logStream(inputMessage.getBody());
    return super.read(type, contextClass, inputMessage);
}

我可以获取输入流,但如果我读取内容,它会变为空并且我会丢失消息。此外,不支持 mark() 和 reset()。它是由 PushbackInputStream 实现的,所以我尝试读取它的内容并将其推送回来:

public void logStream(InputStream is) {
    if (is instanceof PushbackInputStream)
    try {
        PushbackInputStream pushbackInputStream = (PushbackInputStream) is;
        byte[] bytes = new byte[20000];
        StringBuilder sb = new StringBuilder(is.available());
        int red = is.read();
        int pos =0;
        while (red > -1) {
            bytes[pos] = (byte) red;
            pos=1 + pos;
            red = is.read();
        }
        pushbackInputStream.unread(bytes,0, pos-1);
        log.info("Json payload " + sb.toString());
    } catch (Exception e) {
        log.error("ignoring exception in logger ", e);
    }
}

但我得到异常

java.io.IOException: Push back buffer is full

我还尝试打开 http 级别的日志记录,如下所述:Spring RestTemplate - how to enable full debugging/logging of requests/responses? 没有运气。

【问题讨论】:

    标签: java spring spring-mvc logging slf4j


    【解决方案1】:

    听起来你想装饰HttpInputMessage,所以它返回一个装饰过的InputStream,它将所有读取记录在一个内部缓冲区中,然后在close()finalize() 记录读取的内容。

    这是一个 InputStream,它将捕获读取的内容:

      public class LoggingInputStream extends FilterInputStream {
    
          private ByteArrayOutputStream out = new ByteArrayOutputStream();
          private boolean logged = false;
    
          protected LoggingInputStream(InputStream in) {
              super(in);
          }
    
          @Override
          protected void finalize() throws Throwable {
              try {
                  this.log();
              } finally {
                  super.finalize();
              }
          }
    
          @Override
          public void close() throws IOException {
              try {
                  this.log();
              } finally {
                  super.close();
              }
          }
    
          @Override
          public int read() throws IOException {
              int r = super.read();
              if (r >= 0) {
                  out.write(r);
              }
              return r;
          }
    
          @Override
          public int read(byte[] b) throws IOException {
              int read = super.read(b);
              if (read > 0) {
                  out.write(b, 0, read);
              }
              return read;
          }
    
          @Override
          public int read(byte[] b, int off, int len) throws IOException {
              int read = super.read(b, off, len);
              if (read > 0) {
                  out.write(b, off, read);
              }
              return read;
          }
    
          @Override
          public long skip(long n) throws IOException {
              long skipped = 0;
              byte[] b = new byte[4096];
              int read;
              while ((read = this.read(b, 0, (int)Math.min(n, b.length))) >= 0) {
                  skipped += read;
                  n -= read;
              }
              return skipped;
          }
    
          private void log() {
              if (!logged) {
                  logged = true;
                  try {
                      log.info("Json payload " + new String(out.toByteArray(), "UTF-8");
                  } catch (UnsupportedEncodingException e) { }
              }
          }
      }
    

    现在

    @Override
    public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        return super.read(type, contextClass, new HttpInputMessage() {
            @Override
            public InputStream getBody() {
                return new LoggingInputStream(inputMessage.getBody());
            }
            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
        });
    }
    

    【讨论】:

      【解决方案2】:

      按照 David Ehrmann 的建议装饰 HttpInputMessage 是一种可能的解决方案。

      此功能的全部问题在于它需要多次读取InputStream。但是,这是不可能的,一旦您读取了一部分或流,它就会“被消耗”,并且无法返回并再次读取它。

      一个典型的解决方案是应用一个过滤器,该过滤器将为允许重新读取inputStream 的请求创建一个包装器。一种方法是使用TeeInputStream,它将从InputStream 读取的所有字节复制到辅助OutputStream

      有一个 github 项目使用了这种过滤器,实际上只是用于相同的目的 spring-mvc-logger 使用的 RequestWrapper

      public class RequestWrapper extends HttpServletRequestWrapper {
      
          private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
          private long id;
      
      
          public RequestWrapper(Long requestId, HttpServletRequest request) {
              super(request);
              this.id = requestId;
          }
      
          @Override
          public ServletInputStream getInputStream() throws IOException {
              return new ServletInputStream() {
                  private TeeInputStream tee = new TeeInputStream(RequestWrapper.super.getInputStream(), bos);
      
                  @Override
                  public int read() throws IOException {
                      return tee.read();
                  }
              };
          }
      
          public byte[] toByteArray(){
              return bos.toByteArray();
          }
      
          public long getId() {
              return id;
          }
      
          public void setId(long id) {
              this.id = id;
          }
      }
      

      类似的实现也包装了响应

      【讨论】:

      • 尝试使用它但没有运气。在 CommonsRequestLoggingFilter 中覆盖 doFilterInternal(),其中替换过滤器,添加此 AbstractAnnotationConfigDispatcherServletInitializer,过滤器被实例化,但不调用 doFilterInternal
      • 我终于让它工作了。我需要像 David Ehrmann 所建议的那样在 ServletInputStream 中实现更多方法。作为响应方法 close() 没有被调用,所以我连接到被调用 3 次的 flush(),所以我需要确保它只会记录一次。
      【解决方案3】:

      经过超过一整天的实验,我得到了可行的解决方案。 它由 Logging 过滤器、请求和响应的两个包装器以及 Logging 过滤器的注册组成:

      过滤器类是:

      /**
       * Http logging filter, which wraps around request and response in
       * each http call and logs
       * whole request and response bodies. It is enabled by 
       * putting this instance into filter chain
       * by overriding getServletFilters() in  
       * AbstractAnnotationConfigDispatcherServletInitializer.
       */
      public class LoggingFilter extends AbstractRequestLoggingFilter {
      
      private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);
      
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
              throws ServletException, IOException {
          long id = System.currentTimeMillis();
          RequestLoggingWrapper requestLoggingWrapper = new RequestLoggingWrapper(id, request);
          ResponseLoggingWrapper responseLoggingWrapper = new ResponseLoggingWrapper(id, response);
          log.debug(id + ": http request " + request.getRequestURI());
          super.doFilterInternal(requestLoggingWrapper, responseLoggingWrapper, filterChain);
          log.debug(id + ": http response " + response.getStatus() + " finished in " + (System.currentTimeMillis() - id) + "ms");
      }
      
      @Override
      protected void beforeRequest(HttpServletRequest request, String message) {
      
      }
      
      @Override
      protected void afterRequest(HttpServletRequest request, String message) {
      
      }
      }
      

      这个类正在使用流包装器,这是由 奴隶大师和大卫·埃尔曼。

      请求包装器如下所示:

      /**
       * Request logging wrapper using proxy split stream to extract request body
       */
      public class RequestLoggingWrapper extends HttpServletRequestWrapper {
      private static final Logger log =  LoggerFactory.getLogger(RequestLoggingWrapper.class);
      private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
      private long id;
      
      /**
       * @param requestId and id which gets logged to output file. It's used to bind request with
       *                  response
       * @param request   request from which we want to extract post data
       */
      public RequestLoggingWrapper(Long requestId, HttpServletRequest request) {
          super(request);
          this.id = requestId;
      }
      
      @Override
      public ServletInputStream getInputStream() throws IOException {
          final ServletInputStream servletInputStream = RequestLoggingWrapper.super.getInputStream();
          return new ServletInputStream() {
              private TeeInputStream tee = new TeeInputStream(servletInputStream, bos);
      
              @Override
              public int read() throws IOException {
                  return tee.read();
              }
      
              @Override
              public int read(byte[] b, int off, int len) throws IOException {
                  return tee.read(b, off, len);
              }
      
              @Override
              public int read(byte[] b) throws IOException {
                  return tee.read(b);
              }
      
              @Override
              public boolean isFinished() {
                  return servletInputStream.isFinished();
              }
      
              @Override
              public boolean isReady() {
                  return servletInputStream.isReady();
              }
      
              @Override
              public void setReadListener(ReadListener readListener) {
                  servletInputStream.setReadListener(readListener);
              }
      
              @Override
              public void close() throws IOException {
                  super.close();
                  // do the logging
                  logRequest();
              }
          };
      }
      
      public void logRequest() {
          log.info(getId() + ": http request " + new String(toByteArray()));
      }
      
      public byte[] toByteArray() {
          return bos.toByteArray();
      }
      
      public long getId() {
          return id;
      }
      
      public void setId(long id) {
          this.id = id;
      }
      }
      

      响应包装器仅在关闭/刷新方法中有所不同(关闭不会被调用)

      public class ResponseLoggingWrapper extends HttpServletResponseWrapper {
      private static final Logger log = LoggerFactory.getLogger(ResponseLoggingWrapper.class);
      private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
      private long id;
      
      /**
       * @param requestId and id which gets logged to output file. It's used to bind response with
       *                  response (they will have same id, currenttimemilis is used)
       * @param response  response from which we want to extract stream data
       */
      public ResponseLoggingWrapper(Long requestId, HttpServletResponse response) {
          super(response);
          this.id = requestId;
      }
      
      @Override
      public ServletOutputStream getOutputStream() throws IOException {
          final ServletOutputStream servletOutputStream = ResponseLoggingWrapper.super.getOutputStream();
          return new ServletOutputStream() {
              private TeeOutputStream tee = new TeeOutputStream(servletOutputStream, bos);
      
              @Override
              public void write(byte[] b) throws IOException {
                  tee.write(b);
              }
      
              @Override
              public void write(byte[] b, int off, int len) throws IOException {
                  tee.write(b, off, len);
              }
      
              @Override
              public void flush() throws IOException {
                  tee.flush();
                  logRequest();
              }
      
              @Override
              public void write(int b) throws IOException {
                  tee.write(b);
              }
      
              @Override
              public boolean isReady() {
                  return servletOutputStream.isReady();
              }
      
              @Override
              public void setWriteListener(WriteListener writeListener) {
                  servletOutputStream.setWriteListener(writeListener);
              }
      
      
              @Override
              public void close() throws IOException {
                  super.close();
                  // do the logging
                  logRequest();
              }
          };
      }
      
      public void logRequest() {
          byte[] toLog = toByteArray();
          if (toLog != null && toLog.length > 0)
              log.info(getId() + ": http response " + new String(toLog));
      }
      
      /**
       * this method will clear the buffer, so
       *
       * @return captured bytes from stream
       */
      public byte[] toByteArray() {
          byte[] ret = bos.toByteArray();
          bos.reset();
          return ret;
      }
      
      public long getId() {
          return id;
      }
      
      public void setId(long id) {
          this.id = id;
      }
      

      }

      最后 LoggingFilter 需要像这样在 AbstractAnnotationConfigDispatcherServletInitializer 中注册:

       @Override
      protected Filter[] getServletFilters() {
          LoggingFilter requestLoggingFilter = new LoggingFilter();
      
          return new Filter[]{requestLoggingFilter};
      }
      

      我知道,这里有 maven lib,但我不想包含整个 lib,因为日志记录实用程序很小。这比我最初想象的要难得多。我希望通过修改 log4j.properties 来实现这一点。我仍然认为这应该是 Spring 的一部分。

      【讨论】:

        猜你喜欢
        • 2019-07-10
        • 2012-07-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-10-09
        • 2012-09-26
        • 1970-01-01
        • 2018-06-02
        相关资源
        最近更新 更多