大家好,又见面了,我是你们的朋友全栈君。
最近写http接口时,有了很多关于接口取参方式的疑问,大家都知道 Servlet常用的取参方式有 getParameter、getParameterValues、getInputStream(读流形式)。SpringMvc 常用的有封装好的 @RequestParam ,RequestBody 。这些取参方式都有什么特点,我都写了测试接口,利用postman 做了测试。通过测试现象得到了如下结论,如有错误,请指正。
HttpServletRequest
1、getParameter() 取 Key- Value形式的值(URL带参+Form Data) 相同Key只取第一个值,且优先取 url上带参的值。
2、getParameterValues() 取Key – Value全部值, 取Key的所有值。
3、Get方式 以 Query String Paramters 形式传参(参数在URL上),报文体中无值 无法用 getInputStream() 取报文值。
4、Post方式 application/x-www-form-urlencoded 形式 getParameter() 和 getInputStream() 都可取到值,且 getParameter() 和 getInputStream() 互斥。
5、Post方式 application/json 形式 ,只能由 getInputStream()读取。
SpringMvc
1、@RequestParam String param : 读取 Key – Value 形式的值 ,与 getParameterValues() 一样取key的所有值
2、@RequestBody String param : Query String Paramters + application/x-www-form-urlencoded 的 Key -Value形式,全部读取 例如:
<– // 接口请求报文 POST /test/testPost10?name=123&password=456 HTTP/1.1 Host: 127.0.0.1:8080 Content-Type: application/x-www-form-urlencoded Cache-Control: no-cache Postman-Token: 20c00b49-28fb-5556-253b-1be56a6f251c
name=abc&password=efg
–> // 接口取到的值 name=123&name=abc&password=456&password=efg
3、@RequestBody String param : Query String Paramters + application/json 只读取 报文体中的值 例如:
<– POST /test/testPost10?name=123&password=456&token=wsx HTTP/1.1 Host: 127.0.0.1:8080 Content-Type: application/json Cache-Control: no-cache Postman-Token: c1d1bb38-04f5-2872-0488-89a7085e01be
{ “name”:”YHN”, “password”:”IJN” }
–> { “name”:”YHN”, “password”:”IJN” }
4、@RequestBody Object obj 只读取 application/json 形式的报文
探本朔源,我们通过源码来验证上面得到测试结论。
直接在web项目中debug 会找到org.apache.catalina.connector.RequestFacade.java 这个类,是在tomcat中实现的。直接在github上 下载 tomcat 源码(我下的是最新版 tomcat9)
RequestFacade 中有 getParameter 和 getParameterValues方法 ,其中调用了 org.apache.catalina.connector.Request.java 中的方法这两个方法。我们直接去看 org.apache.catalina.connector.Request.java 中的 getParameter(String name) 和getParameterValues(String name) 方法。截取代码如下:
/**
* @return the value of the specified request parameter, if any; otherwise,
* return <code>null</code>. If there is more than one value defined,
* return only the first one.
*
* @param name Name of the desired request parameter
*/
@Override
public String getParameter(String name) {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameter(name);
}
/**
* @return the defined values for the specified request parameter, if any;
* otherwise, return <code>null</code>.
*
* @param name Name of the desired request parameter
*/
@Override
public String[] getParameterValues(String name) {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameterValues(name);
}
其中有一个关键函数 parseParameters() 附上解析代码
/**
* Parse request parameters.
*/
protected void parseParameters() {
parametersParsed = true; // 设置解析标志 ,后面再使用getParamter()时不再次解析
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
parameters.setLimit(getConnector().getMaxParameterCount());
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset); // 设置字符集
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If !useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoyeAdapter.service()
// 1、解析 Query String Paramters形式(url带参形式)
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false); // 解析 multipart/form-data
success = true;
return;
}
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
// 2、根据上面的 contentType 判断 下面解析 application/x-www-form-urlencoded 形式
int len = getContentLength(); // 报文体的长度 get方式 无报文体
if (len > 0) {
int maxPostSize = connector.getMaxPostSize();
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
byte[] formData = null;
if (len < CACHED_POST_LEN) {
if (postData == null) {
postData = new byte[CACHED_POST_LEN];
}
formData = postData;
} else {
formData = new byte[len];
}
try {
if (readPostBody(formData, len) != len) { // readPostBody方法(代码已截取)读取报文体的流 使用getStream().read() 方法 。注意:流只能读取一次。
parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
return;
}
} catch (IOException e) {
// Client disconnect
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len); // 解析application/x-www-form-urlencoded
} else if ("chunked".equalsIgnoreCase(
coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
/**
* Read post body in an array.
*
* @param body The bytes array in which the body will be read
* @param len The body length
* @return the bytes count that has been read
* @throws IOException if an IO exception occurred
*/
protected int readPostBody(byte[] body, int len)
throws IOException {
int offset = 0;
do {
int inputLen = getStream().read(body, offset, len - offset);
if (inputLen <= 0) {
return offset;
}
offset += inputLen;
} while ((len - offset) > 0);
return len;
}
从上面的代码可知 :Parameters 中有两个方法去解析 paramter 的值 1、parameters.handleQueryParameters(); 2、parameters.processParameters(formData, 0, len);
我们找到 org.apache.tomcat.util.http.Parameters.java 看这两个方法及相关方法:
/** Process the query string into parameters 解析 query string 即Url带参
*/
public void handleQueryParameters() {
if (didQueryParameters) {
return;
}
didQueryParameters = true;
if (queryMB == null || queryMB.isNull()) {
return;
}
if(log.isDebugEnabled()) {
log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
}
try {
decodedQuery.duplicate(queryMB);
} catch (IOException e) {
// Can't happen, as decodedQuery can't overflow
e.printStackTrace();
}
processParameters(decodedQuery, queryStringCharset); // 解析
}
public void processParameters( byte bytes[], int start, int len ) {
processParameters(bytes, start, len, charset);
}
private void processParameters(byte bytes[], int start, int len, Charset charset) {
if(log.isDebugEnabled()) {
log.debug(sm.getString("parameters.bytes",
new String(bytes, start, len, DEFAULT_BODY_CHARSET)));
}
int decodeFailCount = 0;
int pos = start;
int end = start + len;
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 '=':
if (parsingName) {
// Name finished. Value starts from next character
nameEnd = pos;
parsingName = false;
valueStart = ++pos;
} else {
// Equals character in value
pos++;
}
break;
case '&':
if (parsingName) {
// Name finished. No value.
nameEnd = pos;
} else {
// Value finished
valueEnd = pos;
}
parameterComplete = true;
pos++;
break;
case '%':
case '+':
// Decoding required
if (parsingName) {
decodeName = true;
} else {
decodeValue = true;
}
pos ++;
break;
default:
pos ++;
break;
}
} while (!parameterComplete && pos < end);
if (pos == end) {
if (nameEnd == -1) {
nameEnd = pos;
} else if (valueStart > -1 && valueEnd == -1){
valueEnd = pos;
}
}
....省略代码.... //看上面的 switch() 方法 和 addParameter(name, value) 方法
try {
String name;
String value;
if (decodeName) {
urlDecode(tmpName);
}
tmpName.setCharset(charset);
name = tmpName.toString();
if (valueStart >= 0) {
if (decodeValue) {
urlDecode(tmpValue);
}
tmpValue.setCharset(charset);
value = tmpValue.toString();
} else {
value = "";
}
try {
addParameter(name, value);
} catch (IllegalStateException ise) {
....省略代码....
}
} catch (IOException e) {
....省略代码....
}
}
现在已找到最核心的方法了, processParameters() 通过switch() 方法解析 Key – Value 值 在调用 addParameter(name, value)方法, 下面来看 addParameter()方法:
public void addParameter( String key, String value )
throws IllegalStateException {
if( key==null ) {
return;
}
parameterCount ++;
if (limit > -1 && parameterCount > limit) {
// Processing this parameter will push us over the limit. ISE is
// what Request.parseParts() uses for requests that are too big
setParseFailedReason(FailReason.TOO_MANY_PARAMETERS);
throw new IllegalStateException(sm.getString(
"parameters.maxCountFail", Integer.valueOf(limit)));
}
ArrayList<String> values = paramHashValues.get(key); // 根据key 取出一个 List出来 (同一个key,用List保存多个value值 )
if (values == null) {
values = new ArrayList<>(1);
paramHashValues.put(key, values);
}
values.add(value); // 保存value值
}
这一步让我们找到了 parameter 解析后在内存的保存的位置 paramHashValues
private final Map<String,ArrayList<String>> paramHashValues = new LinkedHashMap<>();
final 一个 LinkedHashMap (进入的顺序与被取出的顺序一致)。到了这一步已经可以到如下结论:
HttpServletRequest 调用 getParameter方法 或 getParameterValues方法时,实际是通过 org.apache.catalina.connector.RequestFacade.java 来调用 org.apache.catalina.connector.Request.java中的 getParameter方法 或 getParameterValues方法,其中通过 parseParameters 解析报文 ,先解析Query String Paramters(url带参),再解析 Form -Data 形式的值。最终把解析的报文值存入 org.apache.tomcat.util.http.Paramters.java 中 Map<String,ArrayList<String>> 中。
现在回过头 来看org.apache.tomcat.util.http.Parameters.java 中的 getParameter() 和 getParameterValues 方法
// 取 key的第一个Vaule值
public String getParameter(String name ) {
handleQueryParameters();
ArrayList<String> values = paramHashValues.get(name);
if (values != null) {
if(values.size() == 0) {
return "";
}
return values.get(0);
} else {
return null;
}
}
// 取 key的全部Value值
public String[] getParameterValues(String name) {
handleQueryParameters();
// no "facade"
ArrayList<String> values = paramHashValues.get(name);
if (values == null) {
return null;
}
return values.toArray(new String[values.size()]);
}
现在就可以理解了 为什么 getParameter 老是优先取url上带的参数了。且只取一个值了,getParameterValues 取Key的所有值。
现在 HttpServletRequest 的 结论 1 和 2 已验证完毕。
下一篇 来看看 getInputStream() 和 getReader()。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/163204.html原文链接:https://javaforall.cn