优质博文:IT-BLOG-CN
此篇文章是基于 Tomcat Request Cookie 丢失问题 文章的一个延续
一、Request 跨线程访问问题
问题代码摘要
为了方便选择发起get请求,然后只需要传递一个参数就行,核心步骤是要把request传递到异步线程里面去,调用getParameter再次获取对应入参。
@GetMapping("/getTest")
public String getTest(HttpServletRequest request) {String age = request.getParameter("age");System.out.println("age=" + age);new Thread(() -> {try {Thread.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}String age1 = request.getParameter("age");System.out.println("age1=" + age1);}).start();return "success";
}
获取请求:http://127.0.0.1:8080/getTest?age=18
从控制台你可以看到这样的输出:
age=18
age1=null
当再次发起调用,会看到控制台的输出是这样的:
age=18
age1=null
age=null
age1=null

和上面的问题类似,这里也有一个类似的方法:getParameter
@Override
public String getParameter(String name) {if (!parametersParsed) {parseParameters();}return coyoteRequest.getParameters().getParameter(name);
}
parametersParsed参数初始化是false,进入parseParameters()方法解析参数,将age=18放到paramHashValues这个Map容器中。 后续的重复请求就会省略解析参数的操作。
parseParameters()方法执行完成之后,接着从前面的 paramHashValues容器里面把age对应的18返回回去:
public String getParameter(String name) {handleQueryParameters(); // 这里也需要注意,存在一个类似的逻辑ArrayList<String> values = paramHashValues.get(name);if (values != null) {if (values.size() == 0) {return "";}return values.get(0); // 返回的是 age 的值 18} else {return null;}
}
这里重点看下handleQueryParameters方法的实现:
public void handleQueryParameters() {if (didQueryParameters) {return;}didQueryParameters = true;if (queryMB == null || queryMB.isNull()) {return;}try {decodedQuery.duplicate(queryMB);} catch (IOException e) {e.printStackTrace();}processParameters(decodedQuery, queryStringCharset);
}
这个方法在parseParamters中也会调用:
protected void parseParamters() {parametersParsed = true;Parameters parameters = coyoteRequest.getParameters();boolean success = false;try {// Set this every time in case limit has been changed via JMXparameters.setLimit(getConnector().getMaxParameterCount());//...Charset charset = getCharset();boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();parameters.setCharset(charset);//...// 这里也调用了 handleQueryParameters 方法。parameters.handleQueryParameters();}
}
handleQueryParameters方法才是真正解析参数的方法,为了防止重复解析它加入了这样的逻辑:
if (didQueryParameters) {return;
}didQueryParameters = true;

didQueryParameters初始为false,随后被设置为true。这个和之前的业务逻辑一致,入参解析一次并存放至Map中。
方法叫做recycle,表明是循环再利用,在这里面会把存放参数的Map清空,把didQueryParameters再次设置为了false。org.apache.tomcat.util.http.Parameters#recycle方法如下:
public void recycle() {parameterCount = 0;paramHashValues.clear();didQueryParameters = false;charset = DEFAULT_BODY_CHARSET;decodeQuery.recycle();parseFailedReason = null;
}
而当你用同样的手段去观察parametersParsed参数,也就是这个参数的时候,会发现它也有一个recycle方法:org.apache.catalina.connector.Request#recycle
public void recycle() {internalDispatcherType = null;requestDispatcherPath = null;authType = null;inputBuffer.recycle();usingInputStream = false;usingReader = false;userPrincipal = null;parametersParsed = false;
}
由于我们在异步线程里面还触发了一次getParameter方法:但是getTest方法已经完成了响应,这个时候Request可能已经完成了回收。为了避免这个“可能”,我添加了sleep,保证request完成回收。
@GetMapping("/getTest")
public String getTest(HttpServletRequest request) {String age = request.getParameter("age");System.out.println("age=" + age);new Thread(() -> {try {Thread.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}String age1 = request.getParameter("age");System.out.println("age1=" + age1);}).start();return "success";
}
再次触发handleQueryParameters的时候,didQueryParameters由于被recycle了,所以变成了false。
然后执行解析的逻辑,把didQueryParameters设置为true。
但是,我们可以看到,此时查询的内容却没有了,是个null:这里的null也很好理解,肯定是随着调用结束,被recycle了。
为什么再次发起请求的时候,都返回null。
因为Tomcat的Rquest使用的是池化思想,如果你拿到的是上一次的Request请求,那么因为在异步线程里面调用getParameter的时候,把didQueryParameters设置为true了。
但是异步线程里面的调用,超出了request的生命周期,所以并不会再次触发request的recycle相关操作,因此这个request拿来复用的时候didQueryParameters还是true。
所以,第二次请求的入参有值的,但是没用啊,didQueryParameters是true,程序直接return了,不会去解析你的入参:
二、Request 的生命周期
每个request对象只在servlet的服务方法的范围内有效,或者在过滤器的doFilter方法的范围内有效。
但是组件的异步处理功能被启用后,并且在request上调用了startAsync方法后比较特殊。我们先看下startAsync方法:
public AsyncContext startAsync() throws IllegalStateException
在发生异步处理的情况下,request对象的生命周期一直会延续到在AsyncContext上调用complete方法之前。
/*** Completes the async request processing and closes the response stream*/
void complete();
也就是说如果需要在上述范围之外,也就是多线程中使用request对象,需要使用到如下两个方法:
【1】request的startAsync方法;
【2】AsyncContext的complete方法;
我们将之前的代码进行改造:
@GetMapping("/getTest")
public String getTest(HttpServletRequest request, HttpServletResponse response) {AsycnContext asycnContext = request.startAsync(request, response);String age = request.getParameter("age");System.out.println("age=" + age);new Thread(() -> {try {Thread.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}String age1 = request.getParameter("age");System.out.println("age1=" + age1);asycnContext.complete();}).start();return "success";
}
此时在进行调用的时候,就算调用两次,都会正常输出:
age=18
age1=18
age=18
age1=18
从现象上来说,就是getTest请求返回之后,request线程并没有被调用recycle方法进行回收。
在recycle方法的调用链上很快就能找到这个方法:
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status) throws IOException {// ......if (dispatches != null) {// ......} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {state = dispatch(status);state = checkForPipelinedData(state, socketWrapper);}
}
从complete()方法上面的注解closes the response stream也不难发现,只有调用complete()方法之后,response流才会关闭。
