目錄
對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()。這麼好的裝*機會怎麼能錯過!
以上