我被要求开发一个Tomcat阀门,记录所有HTTP请求,包括它们的身体。由于包含正文的流只能读取一次,我发现我需要包装请求。我在这里找到了一个基于JBOSS的例子(下载链接" Maven项目的阀门,用于转移与身体的完整请求"):
https://bz.apache.org/bugzilla/show_bug.cgi?id=45014
我将其改编为使用vanilla 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 Manager应用程序并将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:
添加了一个示例项目
答案 0 :(得分:1)
除getInputStream
外,您还应覆盖getReader
。看起来您的目标应用程序更喜欢使用阅读器,这就是您从未调用输入流的原因。
答案 1 :(得分:1)
总结我们在评论中的讨论:
只需调用getInputStream()
的{{1}}或getReader()
方法即可更改该请求的内部状态。首先,它将导致异常调用反之亦然方法(访问流后无法访问读取器,反之亦然)。它还会导致受保护的request
方法的不同行为。如果曾经调用过任何一种方法,这将中途中止。
这可以在这里看到:Code of Request-class
在第1175行,我们可以看到阻止调用getReader的逻辑,一旦调用了getStream,在第2752行,如果调用了任何一种方法,你可以看到parseParameters()
在中间停止。 / p>
我想,这是你烦恼的原因。