我被要求开发一个 Tomcat 阀门来记录所有 HTTP 请求,包括它们的主体。由于包含主体的流只能读取一次,我发现我需要包装请求。我在这里找到了一个基于 JBOSS 的示例(下载链接“Maven project for a Valve that dumps full request with body”):
https://bz.apache.org/bugzilla/show_bug.cgi?id=45014
我对它进行了调整,使其适用于原版 Tomcat 和更多最新的 API(我使用的是 tomcat-catalina:8.5.20)。
这是我的阀门的样子:
public class CaptureValve extends ValveBase {
// ...
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Wrap request so the body can be read multiple times
RequestWrapper wrappedRequest = new RequestWrapper(request);
// important - otherwise, requests aren't passed further down the chain...
getNext().invoke(wrappedRequest, response);
// Simplified for demo purposes - now I'm reading the body to log it
LogBody(wrappedRequest.getBody());
}
// ...
}
现在RequestWrapper
,正如您想象的那样,它只是代理对包装对象的调用,除了GetRequest
: 以下是该类的相关部分:
public class RequestWrapper extends Request {
//...
public RequestWrapper(Request wrapped) throws IOException {
wrappedCatalinaRequest = wrapped;
loggingRequest = new LoggingRequest(wrapped);
}
//...
@Override
public HttpServletRequest getRequest() {
// here is where the actual request used to read from is retrieved
logger.info("getRequest()");
return loggingRequest;
}
//...
}
所以下一部分是LoggingRequest
包装内部的RequestFacade
:
private class LoggingRequest extends RequestFacade {
private LoggingInputStream is;
LoggingRequest(Request request) throws IOException {
super(request);
int len = 0;
try {
len = Integer.parseInt(request.getHeader("content-length"));
} catch (NumberFormatException e) {
// ignore and assume 0 length
}
String contentType = request.getHeader("content-type");
if (contentType != null) {
for (String ct : contentType.split(";")) {
String s = ct.trim();
if (s.startsWith("charset")) {
charset = s.substring(s.indexOf("=") + 1);
break;
}
}
}
// This line causes the issues I describe below
is = new LoggingInputStream(request.getRequest().getInputStream(), len, charset);
}
@Override
public ServletInputStream getInputStream() throws IOException {
logger.info("LoggingRequest.getInputStream()");
return is;
}
@Override
public BufferedReader getReader() throws IOException {
logger.info("LoggingRequest.getReader()");
return new BufferedReader(new InputStreamReader(is, charset));
}
public String getPayload() {
logger.info("Method: " + new Object() {}.getClass().getEnclosingMethod().getName());
return is.getPayload();
}
}
请注意我将输入流分配给is
变量的行。这就是我在下面描述的问题开始的地方。
最后,包装器ServletInputStream
- 如您所见,其想法是,当从实际的 Tomcat 应用程序读取主体时,将读取的字节也写入缓冲区,然后可以通过该getPayload()
方法再次读取该缓冲区。我剥离了代码的明显部分,如果您想查看所有详细信息,您可以在链接的示例项目中找到它,我从中获得了所有这些:
public class LoggingInputStream extends ServletInputStream {
//...
public LoggingInputStream(ServletInputStream inputStream, int length, String charset) {
super();
is = inputStream;
bytes = new ByteArrayOutputStream(length);
charsetName = (charset == null ? "UTF-8" : charset);
}
/*
* Since we are not sure which method will be used just override all 4 of them:
*/
@Override
public int read() throws IOException {
logger.info("LoggingInputStream.read()");
int ch = is.read();
if (ch != -1) {
bytes.write(ch);
// logger.info("read:" + ch);
// logger.info("bytes.size()=" + bytes.size());
}
return ch;
}
@Override
public int read(byte[] b) throws IOException {
logger.info("LoggingInputStream.read(byte[] b)");
// logger.info("byte[].length=" + b.length);
// logger.info("byte[]=" + b);
int numBytesRead = is.read(b);
if (numBytesRead != -1) {
for (int i = 0; i < numBytesRead; i++) {
bytes.write(b[i]);
}
}
return numBytesRead;
}
@Override
public int read(byte[] b, int o, int l) throws IOException {
logger.info("LoggingInputStream.read(byte[] b, int o, int l)");
int numBytesRead = is.read(b, o, l);
if (numBytesRead != -1) {
for (int i = o; i < numBytesRead; i++) {
bytes.write(b[i]);
}
}
return numBytesRead;
}
@Override
public int readLine(byte[] b, int o, int l) throws IOException {
logger.info("LoggingInputStream.readLine(byte[] b, int o, int l)");
int numBytesRead = is.readLine(b, o, l);
if (numBytesRead != -1) {
for (int i = o; i < numBytesRead; i++) {
bytes.write(b[i]);
}
}
return numBytesRead;
}
@Override
public boolean isFinished() {
logger.info("isFinished");
try {
return is.available() == 0;
}
catch (IOException ioe) {
return false;
}
}
@Override
public boolean isReady() {
logger.info("isReady");
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
public String getPayload() {
if (bytes.size() > 0) {
try {
sb.append(bytes.toString(charsetName));
} catch (UnsupportedEncodingException e) {
sb.append("Error occurred when attempting to read request body with charset '").append(charsetName).append("': ");
sb.append(e.getMessage());
}
}
return sb.toString();
}
}
到目前为止一切顺利,我得到了这个实际工作。我编写了一个非常简单的 Spring 应用程序,其中包含一个基本的 POST 请求方法,我从 Postman 调用它来测试它。这很简单:
public String testPost(String body) {
return body;
}
我用我的 Postman 测试请求发送了一个正文,我得到了我从电话中发回的正文 - 我的阀门也能够读取正文并记录它。
但是当我想将它与应该使用的实际 Tomcat 应用程序一起使用时,它就不起作用了。该应用程序似乎无法再读取请求的正文。我可以在我的日志中看到流的read()
方法从未被调用。所以我尝试了另一个应用程序——为此我只是使用了 Tomcat 管理器应用程序并将 Web 应用程序的会话过期设置为另一个值(这也是一个非常简单的 POST 请求)。它也不起作用......包含新超时值的主体永远不会到达 Tomcat 应用程序。但它适用于我自己的 Spring 应用程序。
还记得我上面提到的这条线吗?
is = new LoggingInputStream(request.getRequest().getInputStream(), len, charset);
我将这一行作为原因进行了跟踪 - 只要我在该行中发表评论,无论我是否注释掉该行之后的任何代码,都会出现问题 - 目标应用程序现在无法再读取流. 但我只在这里获取请求对象引用并将其分配给另一个变量。我实际上并没有在这里阅读流。
我有点迷茫,如果有任何想法在这里可能有问题,我会很高兴。
哦,目标 tomcat 版本是 8.0.46,而我使用的是 9.0 和 8.5(在所有三个上进行了测试,结果相同)。
编辑:对我的包装对象的记录调用
RequestWrapper.<init> ctor RequestWrapper
RequestWrapper$LoggingRequest.<init> ctor LoggingRequest
LoggingInputStream.<init> LoggingInputStream length: 7
RequestWrapper.getContext Method: getContext
RequestWrapper.isAsyncSupported Method: isAsyncSupported
RequestWrapper.isAsync Method: isAsync
RequestWrapper.isAsyncDispatching Method: isAsyncDispatching
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getUserPrincipal Method: getUserPrincipal
RequestWrapper.getSessionInternal Method: getSessionInternal
RequestWrapper.getWrapper Method: getWrapper
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getMethod Method: getMethod
RequestWrapper.getMethod Method: getMethod
RequestWrapper.getUserPrincipal Method: getUserPrincipal
RequestWrapper.getNote Method: getNote
RequestWrapper.getCoyoteRequest Method: getCoyoteRequest
RequestWrapper.getCoyoteRequest Method: getCoyoteRequest
RequestWrapper.setAuthType Method: setAuthType
RequestWrapper.setUserPrincipal Method: setUserPrincipal
RequestWrapper.getSessionInternal Method: getSessionInternal
RequestWrapper.getContext Method: getContext
RequestWrapper.changeSessionId Method: changeSessionId
RequestWrapper.getPrincipal Method: getPrincipal
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getWrapper Method: getWrapper
RequestWrapper.isAsyncSupported Method: isAsyncSupported
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getDispatcherType Method: getDispatcherType
RequestWrapper.setAttribute Method: setAttribute
RequestWrapper.setAttribute Method: setAttribute
RequestWrapper.getFilterChain Method: getFilterChain
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.isAsyncDispatching Method: isAsyncDispatching
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.isAsync Method: isAsync
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getBody Method: getBody
RequestWrapper$LoggingRequest.getPayload Method: getPayload
LoggingInputStream.getPayload getPayload size: 0
LoggingInputStream.getPayload getPayload result:
编辑:我在https://github.com/codekoenig/RequestLoggerValve添加了一个示例项目