目录
对application/x-www-form-urlencoded请求参数的处理
前言
我们用@RequestMapping标识一个Web请求的映射,可以标识在方法上,当我们向服务器发送一个请求时,由Spring解析请求来的参数,并赋值给方法的参数,比如这样
@RequestMapping(value = "/testRequestMapping", method = RequestMethod.POST)
public void testRequestMapping (String para) {
//
}
本文关注的是Spring对请求的参数进行封装,并最终转换成java方法的参数的过程。(也就是把请求中的para参数转换成testRequestMapping方法中的para参数)
当请求的ContentType是form-data和x-www-form-urlencoded时,Spring对参数的接收和转换方式不同。
Spring对请求参数的处理方法
Spring把请求的参数名和参数值最终保存在了一个LinkedHashMap中,封装关系如下:
1,Spring对请求参数的处理方法来自org.apache.catalina.connector.Request类的parseParameters()方法,这个Request类在tomcat-embed-core-8.5.29.jar下。
2,org.apache.catalina.connector.Request类里有一个org.apache.coyote.Request对象。coyote是草原狼,看来他们真的很喜欢动物。
3,org.apache.coyote.Request类里有一个org.apache.tomcat.util.http.Parameters对象。
4,org.apache.tomcat.util.http.Parameters类里维护了一个属性:paramHashValues,类型是LinkedHashMap,请求的参数名会被映射成Map的key,参数值被映射成Map的value,然后保存在这个Map中。
5,当调用request. getParameterValues()方法时,就是从这个Map中获取参数的。
6,被@RequestMapping标注的方法,是由Spring生成代理并执行的,此时方法中的参数值也是从Map中获取的。
调用到parseParameters()方法时的调用栈信息是这样的:
parseParameters:3216, Request (org.apache.catalina.connector)
getParameter:1137, Request (org.apache.catalina.connector)
getParameter:381, RequestFacade (org.apache.catalina.connector)
doFilterInternal:75, HiddenHttpMethodFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:200, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:198, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:496, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:140, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:803, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:790, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1459, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
parseParameters()方法代码如下:
protected void parseParameters() {
this.parametersParsed = true;
Parameters parameters = this.coyoteRequest.getParameters();
boolean success = false;
try {
parameters.setLimit(this.getConnector().getMaxParameterCount());
Charset charset = this.getCharset();
boolean useBodyEncodingForURI = this.connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
parameters.handleQueryParameters();
if (this.usingInputStream || this.usingReader) {
success = true;
return;
}
if (this.getConnector().isParseBodyMethod(this.getMethod())) {
String contentType = this.getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(59);
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
this.parseParts(false);
success = true;
return;
}
if (!"application/x-www-form-urlencoded".equals(contentType)) {
success = true;
return;
}
int len = this.getContentLength();
if (len <= 0) {
if ("chunked".equalsIgnoreCase(this.coyoteRequest.getHeader("transfer-encoding"))) {
Object var21 = null;
Context context;
byte[] formData;
try {
formData = this.readChunkedPostBody();
} catch (IllegalStateException var17) {
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
context = this.getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var17);
}
return;
} catch (IOException var18) {
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
context = this.getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var18);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
} else {
int maxPostSize = this.connector.getMaxPostSize();
Context context;
if (maxPostSize >= 0 && len > maxPostSize) {
context = this.getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.postTooLarge"));
}
this.checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
context = null;
byte[] formData;
if (len < 8192) {
if (this.postData == null) {
this.postData = new byte[8192];
}
formData = this.postData;
} else {
formData = new byte[len];
}
try {
if (this.readPostBody(formData, len) != len) {
parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
return;
}
} catch (IOException var19) {
Context context = this.getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var19);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
}
success = true;
return;
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
重点关注一下这一部分:
if ("multipart/form-data".equals(contentType)) {
this.parseParts(false);
success = true;
return;
}
if (!"application/x-www-form-urlencoded".equals(contentType)) {
success = true;
return;
}
从这一部分可以看到,Spring对multipart/form-data和application/x-www-form-urlencoded两种ContentType的请求,采用了不同的参数处理方式。
对multipart/form-data请求,调用了
this.parseParts(false);
然后如果ContentType也不是application/x-www-form-urlencoded就直接退出了,注意第二个if前面有个叹号。
所以这段代码之后的部分都是对application/x-www-form-urlencoded类型的处理了。
下面分别看一下Spring是怎么处理两种不同请求的参数的。
对multipart/form-data请求参数的处理
如前面所说,当ContentType是multipart/form-data时,调用的是this.parseParts(false);方法,这个方法的代码如下:
private void parseParts(boolean explicit) {
if (this.parts == null && this.partsParseException == null) {
Context context = this.getContext();
MultipartConfigElement mce = this.getWrapper().getMultipartConfigElement();
if (mce == null) {
if (!context.getAllowCasualMultipartParsing()) {
if (explicit) {
this.partsParseException = new IllegalStateException(sm.getString("coyoteRequest.noMultipartConfig"));
return;
}
this.parts = Collections.emptyList();
return;
}
mce = new MultipartConfigElement((String)null, (long)this.connector.getMaxPostSize(), (long)this.connector.getMaxPostSize(), this.connector.getMaxPostSize());
}
Parameters parameters = this.coyoteRequest.getParameters();
parameters.setLimit(this.getConnector().getMaxParameterCount());
boolean success = false;
try {
String locationStr = mce.getLocation();
File location;
if (locationStr != null && locationStr.length() != 0) {
location = new File(locationStr);
if (!location.isAbsolute()) {
location = (new File((File)context.getServletContext().getAttribute("javax.servlet.context.tempdir"), locationStr)).getAbsoluteFile();
}
} else {
location = (File)context.getServletContext().getAttribute("javax.servlet.context.tempdir");
}
if (!location.isDirectory()) {
parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID);
this.partsParseException = new IOException(sm.getString("coyoteRequest.uploadLocationInvalid", new Object[]{location}));
return;
}
DiskFileItemFactory factory = new DiskFileItemFactory();
try {
factory.setRepository(location.getCanonicalFile());
} catch (IOException var29) {
parameters.setParseFailedReason(FailReason.IO_ERROR);
this.partsParseException = var29;
return;
}
factory.setSizeThreshold(mce.getFileSizeThreshold());
ServletFileUpload upload = new ServletFileUpload();
upload.setFileItemFactory(factory);
upload.setFileSizeMax(mce.getMaxFileSize());
upload.setSizeMax(mce.getMaxRequestSize());
this.parts = new ArrayList();
try {
List<FileItem> items = upload.parseRequest(new ServletRequestContext(this));
int maxPostSize = this.getConnector().getMaxPostSize();
int postSize = 0;
Charset charset = this.getCharset();
Iterator i$ = items.iterator();
while(true) {
if (!i$.hasNext()) {
success = true;
break;
}
FileItem item = (FileItem)i$.next();
ApplicationPart part = new ApplicationPart(item, location);
this.parts.add(part);
if (part.getSubmittedFileName() == null) {
String name = part.getName();
String value = null;
try {
value = part.getString(charset.name());
} catch (UnsupportedEncodingException var28) {
;
}
if (maxPostSize >= 0) {
postSize += name.getBytes(charset).length;
if (value != null) {
++postSize;
postSize = (int)((long)postSize + part.getSize());
}
++postSize;
if (postSize > maxPostSize) {
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
throw new IllegalStateException(sm.getString("coyoteRequest.maxPostSizeExceeded"));
}
}
parameters.addParameter(name, value);
}
}
} catch (InvalidContentTypeException var30) {
parameters.setParseFailedReason(FailReason.INVALID_CONTENT_TYPE);
this.partsParseException = new ServletException(var30);
} catch (SizeException var31) {
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
this.checkSwallowInput();
this.partsParseException = new IllegalStateException(var31);
} catch (FileUploadException var32) {
parameters.setParseFailedReason(FailReason.IO_ERROR);
this.partsParseException = new IOException(var32);
} catch (IllegalStateException var33) {
this.checkSwallowInput();
this.partsParseException = var33;
}
} finally {
if (this.partsParseException != null || !success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
}
方法很长,主要流程是这样的:
1,创建一个参数处理器MultipartConfigElement。
2,确定文件上传地址。
3,创建文件上传用的Factory。
4,把请求中的参数按name-value作为一个临时文件的方式依次上传到服务器,每组name-value形成一个临时文件,文件的内容就是value本身,文件名则和name有关。
同时形成FileItem的列表,每个FileItem代表一个文件,也就是代表一组name-value。
也就是这行代码:
List<FileItem> items = upload.parseRequest(new ServletRequestContext(this));
文件的路径由创建参数处理器时的TomcatEmbeddedContext决定,我本地的临时文件是这样的:
C:\Users\pine0\AppData\Local\Temp\tomcat.7096701330036005397.8002\work\Tomcat\localhost\ROOT\upload_7cc5e1e1_48ad_4631_ab1d_4dbcf9df275f_00000000.tmp。
5,循环FileItem列表,从FileItem对象中获得name和value,也就是请求参数名和参数值,并调用
parameters.addParameter(name, value);
方法,把name和value放入Map中。
另外此方法中还有一些比如参数数量的判断,POST参数上限2097152个。
至此,对请求参数的处理结束。
可见,对multipart/form-data请求的参数处理,是先上传文件,再获取参数的。
因为multipart/form-data本身就是可以把文件当参数上传的,可能是考虑到缓存或者方便处理文件类型的参数,所以采用了临时文件的处理方式。
对application/x-www-form-urlencoded请求参数的处理
在parseParameters()方法中两个if之后的部分,就是对application/x-www-form-urlencoded类型参数的处理,处理的大概流程是这样的:
1,获得参数长度。
就是这行代码:
int len = this.getContentLength();
因为ContentType是application/x-www-form-urlencoded时,请求的参数会被组成
pageSize=10&code=&pro=123
这种形式,跟get方式的参数挺像,只不过get方式的参数写在地址里,Post方式的这些参数写在body里。
在这里请求的参数是以byte数组的形式存在的,不是字符串。
2,根据byte数组和len获取请求参数的name-value,然后保存在Map中,也就是这一行代码:
parameters.processParameters(formData, 0, len);
这个方法执行完成后,对application/x-www-form-urlencoded请求参数的处理就结束了。
下面详细介绍一下Spring是如何从byte数组中获得name-value的,processParameters(formData, 0, len)方法的代码如下:
private void processParameters(byte[] bytes, int start, int len, Charset charset) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.bytes", new Object[]{new String(bytes, start, len, DEFAULT_BODY_CHARSET)}));
}
int decodeFailCount = 0;
int pos = start;
int end = start + len;
label172:
while(pos < end) {
int nameStart = pos;
int nameEnd = -1;
int valueStart = -1;
int valueEnd = -1;
boolean parsingName = true;
boolean decodeName = false;
boolean decodeValue = false;
boolean parameterComplete = false;
do {
switch(bytes[pos]) {
case 37:
case 43:
if (parsingName) {
decodeName = true;
} else {
decodeValue = true;
}
++pos;
break;
case 38:
if (parsingName) {
nameEnd = pos;
} else {
valueEnd = pos;
}
parameterComplete = true;
++pos;
break;
case 61:
if (parsingName) {
nameEnd = pos;
parsingName = false;
++pos;
valueStart = pos;
} else {
++pos;
}
break;
default:
++pos;
}
} while(!parameterComplete && pos < end);
if (pos == end) {
if (nameEnd == -1) {
nameEnd = pos;
} else if (valueStart > -1 && valueEnd == -1) {
valueEnd = pos;
}
}
if (log.isDebugEnabled() && valueStart == -1) {
log.debug(sm.getString("parameters.noequal", new Object[]{nameStart, nameEnd, new String(bytes, nameStart, nameEnd - nameStart, DEFAULT_BODY_CHARSET)}));
}
String message;
String value;
if (nameEnd <= nameStart) {
if (valueStart == -1) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.emptyChunk"));
}
} else {
Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
if (valueEnd > nameStart) {
value = new String(bytes, nameStart, valueEnd - nameStart, DEFAULT_BODY_CHARSET);
} else {
value = "";
}
message = sm.getString("parameters.invalidChunk", new Object[]{nameStart, valueEnd, value});
switch(logMode) {
case INFO_THEN_DEBUG:
message = message + sm.getString("parameters.fallToDebug");
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
this.setParseFailedReason(Parameters.FailReason.NO_NAME);
}
} else {
this.tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
this.tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
} else {
this.tmpValue.setBytes(bytes, 0, 0);
}
if (log.isDebugEnabled()) {
try {
this.origName.append(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
this.origValue.append(bytes, valueStart, valueEnd - valueStart);
} else {
this.origValue.append(bytes, 0, 0);
}
} catch (IOException var21) {
log.error(sm.getString("parameters.copyFail"), var21);
}
}
try {
if (decodeName) {
this.urlDecode(this.tmpName);
}
this.tmpName.setCharset(charset);
String name = this.tmpName.toString();
if (valueStart >= 0) {
if (decodeValue) {
this.urlDecode(this.tmpValue);
}
this.tmpValue.setCharset(charset);
value = this.tmpValue.toString();
} else {
value = "";
}
try {
this.addParameter(name, value);
} catch (IllegalStateException var22) {
Mode logMode = maxParamCountLog.getNextMode();
if (logMode != null) {
String message = var22.getMessage();
switch(logMode) {
case INFO_THEN_DEBUG:
message = message + sm.getString("parameters.maxCountFail.fallToDebug");
case INFO:
log.info(message);
break label172;
case DEBUG:
log.debug(message);
}
}
break;
}
} catch (IOException var23) {
this.setParseFailedReason(Parameters.FailReason.URL_DECODING);
++decodeFailCount;
if (decodeFailCount == 1 || log.isDebugEnabled()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.decodeFail.debug", new Object[]{this.origName.toString(), this.origValue.toString()}), var23);
} else if (log.isInfoEnabled()) {
Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
message = sm.getString("parameters.decodeFail.info", new Object[]{this.tmpName.toString(), this.tmpValue.toString()});
switch(logMode) {
case INFO_THEN_DEBUG:
message = message + sm.getString("parameters.fallToDebug");
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
}
this.tmpName.recycle();
this.tmpValue.recycle();
if (log.isDebugEnabled()) {
this.origName.recycle();
this.origValue.recycle();
}
}
}
if (decodeFailCount > 1 && !log.isDebugEnabled()) {
Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String message = sm.getString("parameters.multipleDecodingFail", new Object[]{decodeFailCount});
switch(logMode) {
case INFO_THEN_DEBUG:
message = message + sm.getString("parameters.fallToDebug");
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
也是倍儿长的一段代码,其实逻辑并不复杂,改成伪代码大概是这样的:
while(遍历byte数组){
int pos; byte数组下标
boolean parsingName; 正在处理name,默认true
boolean parameterComplete; 表示一组name-value处理完毕,默认false
int nameStart = pos; name开始的下标
int nameEnd = -1; name结束的下标
int valueStart = -1; value开始的下标
int valueEnd = -1; value结束的下标
while(从pos位置开始遍历byte数组,而且一组name-value没有处理完毕){
如果pos位置是+,(byte=43),说明这个name或者value需要decode一下
如果正在处理name,(parsingName==true),说明这个name需要decode一下
如果正在处理value,(parsingName==false),说明这个value需要decode一下
下标后移
如果pos位置是&,(byte=38),说明一组name-value处理完毕
如果正在处理name,(parsingName==true),则记录name结束的下标
如果正在处理value,(parsingName==false),则记录value结束的下标
parameterComplete设为true
下标后移
如果pos位置是=,(byte=61)
如果正在处理name,(parsingName==true),那么parsingName设为false,记录name结束的下标,下标后移,记录value开始的下标
如果正在处理value,(parsingName==false),则不做处理,下标后移。(看起来是为了支持value中带等号的情况)
如果post位置是其他字符
不做处理,下标后移
}
代码能来到这说明一组name-value处理完毕了
根据name开始和结束的下标获得name字符串,this.tmpName。
根据value开始和结束的下标获得value字符串,this.tmpValue。
如果需要decode一下,就先decode一下。
String name=this.tmpName.toString();
String value = this.tmpValue.toString();
调用addParameter(name, value)添加到Map中
清空this.tmpName和this.tmpValue。
}
其实就是在遍历byte数组的过程中用+=&等符号记录了name和value开始和结束的位置。
byte数组遍历结束,Map也就组装结束了。
另外有个小细节,这里的this.tmpName和this.tmpValue不是字符串类型,而是自定义的一个ByteChunk类,这个类的toString()方法是重写过的:
public String toString() {
if (null == this.buff) {
return null;
} else {
return this.end - this.start == 0 ? "" : StringCache.toString(this);
}
}
可以看到end-start==0时,大概也就是请求中某个name对应的value为空时,返回的value是"",直接取自字符串的常量池,这大概是Spring处理请求的参数时唯一一次直接从常量池获取字符串的场景了,其他的字符串都是new String()的形式获取的。
虽然看起来没什么用,但是我们有以下推论:
当请求的ContentType是application/x-www-form-urlencoded时,如果某参数是空的,那么代码中用该参数和空字符串用==比较的结果是true。
也就是说,如果请求中para参数是空的,那么
@RequestMapping(value = "/testRequestMapping", method = RequestMethod.POST)
public void testRequestMapping (String para) {
System.out.println(para=="");
}
输出的将会是true。
参数不为空时输出的会是false。
ContentType不是application/x-www-form-urlencoded时输出的也是false。
当然,作为遵纪守法的好码农,字符串比较请用equals()。这么好的装*机会怎么能错过!
以上