在 Spring mvc 中记录所有网络流量
Logging all network traffic in Spring mvc
我有 spring 个使用 RequestBody 和 ResponseBody 注释的 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? 但没有成功。
听起来你想修饰 HttpInputMessage
所以它 returns 一个修饰的 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();
}
});
}
按照 David Ehrmann 的建议装饰 HttpInputMessage
是一种可能的解决方案。
此功能的全部问题在于它需要 InputStream
被读取不止一次。然而,这是不可能的,一旦你阅读了一部分或一个流,它 "consumed" 并且没有办法返回并再次阅读它。
一个典型的解决方案是应用一个过滤器,该过滤器将为允许重新读取 inputStream
的请求创建一个包装器。一种方法是使用 TeeInputStream 将从 InputStream
读取的所有字节复制到辅助 OutputStream
.
有一个 github 项目使用了那种过滤器,实际上只是为了相同的目的 spring-mvc-logger RequestWrapper
class 使用了
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;
}
}
类似的实现也包装了响应
经过一整天的试验,我得到了可行的解决方案。
它由日志过滤器、请求和响应的两个包装器以及日志过滤器的注册组成:
过滤器 class 是:
/**
* 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) {
}
}
这个 class 正在使用流包装器,这是由
奴隶主和大卫埃尔曼。
请求包装器如下所示:
/**
* 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 方法中不同(不调用关闭)
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 库,但我不想包含整个库,因为日志记录实用程序很小。这比我原先想象的要难得多。我希望通过修改 log4j.properties 来实现这一点。我仍然认为这应该是 Spring 的一部分。
我有 spring 个使用 RequestBody 和 ResponseBody 注释的 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? 但没有成功。
听起来你想修饰 HttpInputMessage
所以它 returns 一个修饰的 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();
}
});
}
按照 David Ehrmann 的建议装饰 HttpInputMessage
是一种可能的解决方案。
此功能的全部问题在于它需要 InputStream
被读取不止一次。然而,这是不可能的,一旦你阅读了一部分或一个流,它 "consumed" 并且没有办法返回并再次阅读它。
一个典型的解决方案是应用一个过滤器,该过滤器将为允许重新读取 inputStream
的请求创建一个包装器。一种方法是使用 TeeInputStream 将从 InputStream
读取的所有字节复制到辅助 OutputStream
.
有一个 github 项目使用了那种过滤器,实际上只是为了相同的目的 spring-mvc-logger RequestWrapper
class 使用了
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;
}
}
类似的实现也包装了响应
经过一整天的试验,我得到了可行的解决方案。 它由日志过滤器、请求和响应的两个包装器以及日志过滤器的注册组成:
过滤器 class 是:
/**
* 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) {
}
}
这个 class 正在使用流包装器,这是由 奴隶主和大卫埃尔曼。
请求包装器如下所示:
/**
* 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 方法中不同(不调用关闭)
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 库,但我不想包含整个库,因为日志记录实用程序很小。这比我原先想象的要难得多。我希望通过修改 log4j.properties 来实现这一点。我仍然认为这应该是 Spring 的一部分。