记录Spring mvc中的所有网络流量

时间:2015-06-19 16:20:21

标签: java spring spring-mvc logging slf4j

我有使用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?没有运气。

3 个答案:

答案 0 :(得分:4)

经过一整天的实验,我得到了解决方案。 它由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;
}
}

并且响应包装器仅在close / flush方法中有所不同(close不会被调用)

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;
}

}

最后需要在AbstractAnnotationConfigDispatcherServletInitializer中注册LoggingFilter,如下所示:

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

    return new Filter[]{requestLoggingFilter};
}

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

答案 1 :(得分:2)

听起来你想装饰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 :(得分:1)

正如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;
    }
}

类似的实现也包含了响应