问题描述
最近小弟封装了一个新的方法,大概是把请求访问controller层的信息,包括正常的响应和异常信息,通过http请求的方式发送给另外一个接口,之前是通过发送消息给kafka来完成的,改造过程中他除了把请求方式改变了,还把同步的方式改成了启用线程池的异步调用。
public void sendData(HttpServletRequest request, ResponseMessage response) {
executorService.submit(() -> {
Map<String, Object> map = new HashMap<>();
map.put("request", JSON.toJSONString(request.getParameterMap()));
... ...
});
}
这样修改之后,问题随之产生了:
由于这个方法调用频率还是比较高的,类似这样的错误日志每隔几分钟就会出现,于是决定好好分析一下错误原因。
产生原因分析
仔细分析日志,发现请求进来的时候,先进入了项目中的拦截器,而拦截器中是可以拿到参数的,那就很奇怪了,为什么在拦截器里能拿到的参数,访问到controller层的时候却因为参数上有@RequestParam注解而导致报了miss parameter的异常?
分析发现,最近一次上线之后才有了以上的错误日志,于是分析最近提交的代码,怀疑是这个异步读取request的parameter的问题,于是开始追根溯源查源码。
首先查到了Request的源码,因为request是直接调用getParameter的对象:
@Override
public String getParameter(String name) {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameter(name);
}
紧接着,继续追溯coyoteRequest.getParameters().getParameter(name)方法:
public String getParameter(String name ) {
handleQueryParameters();
ArrayList<String> values = paramHashValues.get(name);
if (values != null) {
if(values.size() == 0) {
return "";
}
return values.get(0);
} else {
return null;
}
}
这里看到这个handleQueryParameters()方法引起警觉,继续查看源码:
// -------------------- Processing --------------------
/** Process the query string into parameters
*/
public void handleQueryParameters() {
if (didQueryParameters) {
return;
}
didQueryParameters = true;
if (queryMB == null || queryMB.isNull()) {
return;
}
if(log.isDebugEnabled()) {
log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
}
try {
decodedQuery.duplicate(queryMB);
} catch (IOException e) {
// Can't happen, as decodedQuery can't overflow
e.printStackTrace();
}
processParameters(decodedQuery, queryStringCharset);
}
这个方法的主要作用是将查询串中的参数放到Parameters对象中去,这样request才能通过getParameter()方法拿到参数,这里的didQueryParameters变量就是问题所在了,通过测试发现报异常的时候,这个变量是为true的,而默认值是false,设想完成一次请求后,Parameters对象如果不去reset这些标志位,肯定会影响下次请求的,果然我们在Parameters对象里发现了这个方法:
public void recycle() {
parameterCount = 0;
paramHashValues.clear();
didQueryParameters = false;
charset = DEFAULT_BODY_CHARSET;
decodedQuery.recycle();
parseFailedReason = null;
}
这里会把didQueryParameters重置为false,以便下一个request调用,那么问题又来了,这个方法是谁在调用的呢?继续查源码:
public void recycle() {
bytesRead=0;
contentLength = -1;
contentTypeMB = null;
charset = null;
characterEncoding = null;
expectation = false;
headers.recycle();
serverNameMB.recycle();
serverPort=-1;
localAddrMB.recycle();
localNameMB.recycle();
localPort = -1;
remoteAddrMB.recycle();
remoteHostMB.recycle();
remotePort = -1;
available = 0;
sendfile = true;
serverCookies.recycle();
parameters.recycle();
pathParameters.clear();
uriMB.recycle();
decodedUriMB.recycle();
queryMB.recycle();
methodMB.recycle();
protoMB.recycle();
schemeMB.recycle();
remoteUser.recycle();
remoteUserNeedsAuthorization = false;
authType.recycle();
attributes.clear();
listener = null;
allDataReadEventSent.set(false);
startTime = -1;
}
是由request的recycle()方法调用的,我们这里也可以看到,request完成一次请求调用后,需要重置很多变量和标志位的,然而这还不是最终答案,继续追溯:
/**
* Recycle the input buffer. This should be called when closing the
* connection.
*/
void recycle() {
wrapper = null;
request.recycle();
for (int i = 0; i <= lastActiveFilter; i++) {
activeFilters[i].recycle();
}
byteBuffer.limit(0).position(0);
lastActiveFilter = -1;
parsingHeader = true;
swallowInput = true;
headerParsePos = HeaderParsePosition.HEADER_START;
parsingRequestLine = true;
parsingRequestLinePhase = 0;
parsingRequestLineEol = false;
parsingRequestLineStart = 0;
parsingRequestLineQPos = -1;
headerData.recycle();
}
/**
* End processing of current HTTP request.
* Note: All bytes of the current request should have been already
* consumed. This method only resets all the pointers so that we are ready
* to parse the next HTTP request.
*/
void nextRequest() {
request.recycle();
if (byteBuffer.position() > 0) {
if (byteBuffer.remaining() > 0) {
// Copy leftover bytes to the beginning of the buffer
byteBuffer.compact();
byteBuffer.flip();
} else {
// Reset position and limit to 0
byteBuffer.position(0).limit(0);
}
}
// Recycle filters
for (int i = 0; i <= lastActiveFilter; i++) {
activeFilters[i].recycle();
}
// Reset pointers
lastActiveFilter = -1;
parsingHeader = true;
swallowInput = true;
headerParsePos = HeaderParsePosition.HEADER_START;
parsingRequestLine = true;
parsingRequestLinePhase = 0;
parsingRequestLineEol = false;
parsingRequestLineStart = 0;
parsingRequestLineQPos = -1;
headerData.recycle();
}
这个对象,Http11InputBuffer,给出了我们终极答案,当关闭连接的时候会调用recycle()方法,而处理完当前的http 请求后,还会调用nextRequest()方法,这里的提示让我们豁然开朗,当前的请求必须被已经消费,如果request请求走异步没有结束的话,nextRequest方法是不会被调用的,相关标志位也就不会被重置,而采用线程池的方式处理,有可能由同一线程处理两个request,而第一个request还没有做recycle()处理,导致第二个request的参数无法解析了。
感兴趣的童鞋可以继续追溯源码,看看这个nextRequest()方法到底是由哪里调用的,同步和异步处理到底有什么不同呢?如果同步异步处理方式不同,那应该由两个地方调用nextRequest()方法分别处理才对,下边的代码验证了我们的猜测:
同步调用代码:
if (!isAsync() || getErrorState().isError()) {
request.updateCounters();
if (getErrorState().isIoAllowed()) {
inputBuffer.nextRequest();
outputBuffer.nextRequest();
}
}
异步方法调用:
@Override
protected SocketState dispatchEndRequest() {
if (!keepAlive) {
return SocketState.CLOSED;
} else {
endRequest();
inputBuffer.nextRequest();
outputBuffer.nextRequest();
if (socketWrapper.isReadPending()) {
return SocketState.LONG;
} else {
return SocketState.OPEN;
}
}
}
@Override
public boolean isAsync() {
return asyncStateMachine.isAsync();
}
if (dispatches != null) {
DispatchType nextDispatch = dispatches.next();
state = dispatch(nextDispatch.getSocketStatus());
} else if (status == SocketEvent.DISCONNECT) {
// Do nothing here, just wait for it to get recycled
} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
state = dispatch(status);
if (state == SocketState.OPEN) {
// There may be pipe-lined data to read. If the data isn't
// processed now, execution will exit this loop and call
// release() which will recycle the processor (and input
// buffer) deleting any pipe-lined data. To avoid this,
// process it now.
state = service(socketWrapper);
}
}
这里我们很清楚的看到,同步方法的调用需要判断isAsync()是false才行,如果这个方法是true,说明是异步调用,需要根据异步状态机的状态定义,判断异步调用完成的状态,才可以最终执行dispatchEndRequest()方法,进而调用inputBuffer.nextRequest()方法。我们这里看下异步状态机的定义,作为本文的结尾:
private static enum AsyncState {
DISPATCHED (false, false, false, false),
STARTING (true, true, false, false),
STARTED (true, true, false, false),
MUST_COMPLETE (true, true, true, false),
COMPLETE_PENDING(true, true, false, false),
COMPLETING (true, false, true, false),
TIMING_OUT (true, true, false, false),
MUST_DISPATCH (true, true, false, true),
DISPATCH_PENDING(true, true, false, false),
DISPATCHING (true, false, false, true),
READ_WRITE_OP (true, true, false, false),
MUST_ERROR (true, true, false, false),
ERROR (true, true, false, false);
private final boolean isAsync;
private final boolean isStarted;
private final boolean isCompleting;
private final boolean isDispatching;
private AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting,
boolean isDispatching) {
this.isAsync = isAsync;
this.isStarted = isStarted;
this.isCompleting = isCompleting;
this.isDispatching = isDispatching;
}
public boolean isAsync() {
return isAsync;
}
public boolean isStarted() {
return isStarted;
}
public boolean isDispatching() {
return isDispatching;
}
public boolean isCompleting() {
return isCompleting;
}
}