http接口取参方式 – getParameter 和 getParameterValues[通俗易懂]

2022-09-15 16:05:17 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

前言:

最近写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&amp;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&amp;password=456&amp;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 形式的报文

探本朔源,我们通过源码来验证上面得到测试结论。

getParameter 和 getParameterValues 源码解析

直接在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) 方法。截取代码如下:

代码语言:javascript复制
   /**
     * @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() 附上解析代码

代码语言:javascript复制
   /**
     * 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 看这两个方法及相关方法:

代码语言:javascript复制
/** 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()方法:

代码语言:javascript复制
    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

代码语言:javascript复制
 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 方法

代码语言:javascript复制
   // 取 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

0 人点赞