Spring是怎樣處理form-data和x-www-form-urlencoded請求的參數的

目錄

前言

Spring對請求參數的處理方法

對multipart/form-data請求參數的處理

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

 

以上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章