最近在项目中遇到 由异步执行任务导致的Request中的请求入参对象被不同请求污染的多线程问题;
在同事的研究下,发现了一把Request 在源码层面是如何进行回收处理的;因此将其中涉及到的主要源码进行展示:
首先,根据源码断点我们发现Request采用了包装器模式,而最核心处理的Request是
org.apache.catalina.connector.Request;
我们获取请求参数的方法源码为:
public Map<String, String[]> getParameterMap() {
//当 parameterMap 被锁住的时候,直接返回对象(整个Request生命周期都使用的这个)
if (parameterMap.isLocked()) {
return parameterMap;
}
//当未锁住的时候进行初始化和赋值,并在赋值结束后将其锁住
//在parameterMap 被释放锁之后第一次调用执行(可能被异步调用执行,可能被下一个请求对象执行)
Enumeration<String> enumeration = getParameterNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String[] values = getParameterValues(name);
parameterMap.put(name, values);
}
parameterMap.setLocked(true);
return parameterMap;
}
从我们获取请求Request 中的请求入参可知:
当一次请求结束前,我们将Request交给一个异步执行的线程使用,会出现两种情况
1、在下一次请求前执行getParameterMap() 方法
2、在下一次请求后执行getParameterMap() 方法
而这两种情况都会到来问题:
首先,在下一次请求前执行getParameterMap() 方法 这个时候我们的请求Enumeration<String> enumeration 对象已经被清理,没有任何入参,并且parameterMap对象被锁住,导致下一个请求无法进行参数设置,就会出现前端请求参数无法设置进去的现象;
第二种情况,在下一次请求后执行getParameterMap() 方法,这个时候下一个请求的入参会正常执行,但是因为下一次请求的入参被设置进来,并且我们是引用指向,parameterMap对象并不会被真正的被重新创建,他只是通过Lock控制了数据的安全,因此我们一步请求的线程依然指向下一个接口请求的parameterMap,并且其中的参数值为下一个请求接口的入参值。
为了更便于了解真相,下面我们把Request的重置方法源码贴出来:
public void recycle() {
internalDispatcherType = null;
requestDispatcherPath = null;
authType = null;
inputBuffer.recycle();
usingInputStream = false;
usingReader = false;
userPrincipal = null;
subject = null;
parametersParsed = false;
if (parts != null) {
for (Part part: parts) {
try {
part.delete();
} catch (IOException ignored) {
// ApplicationPart.delete() never throws an IOEx
}
}
parts = null;
}
partsParseException = null;
locales.clear();
localesParsed = false;
secure = false;
remoteAddr = null;
remoteHost = null;
remotePort = -1;
localPort = -1;
localAddr = null;
localName = null;
attributes.clear();
sslAttributesParsed = false;
notes.clear();
recycleSessionInfo();
recycleCookieInfo(false);
//入参内容保存Map,
if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
//处理方式1: new一个,引用地址变更,与老参数引用地址不形成任何影响
parameterMap = new ParameterMap<>();
} else {
//处理方式2:去除锁,清除参数;但是对象的引用不变;
parameterMap.setLocked(false);
parameterMap.clear();
}
mappingData.recycle();
applicationMapping.recycle();
applicationRequest = null;
if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
if (facade != null) {
facade.clear();
facade = null;
}
if (inputStream != null) {
inputStream.clear();
inputStream = null;
}
if (reader != null) {
reader.clear();
reader = null;
}
}
asyncSupported = null;
if (asyncContext!=null) {
asyncContext.recycle();
}
asyncContext = null;
}
其中对parameterMap 的操作代码如下:
//入参内容保存Map,
if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
//处理方式1: new一个,引用地址变更,与老参数引用地址不形成任何影响
parameterMap = new ParameterMap<>();
} else {
//处理方式2:去除锁,清除参数;但是对象的引用不变;
parameterMap.setLocked(false);
parameterMap.clear();
}