Request 跨线程访问问题
优质博文: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 JMX
parameters.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
流才会关闭。