form-data與x-www-form-urlencoded的區(qū)別以及知識(shí)延伸
一、前言
form-data和x-www-form-urlencoded,它們完整的表示是multipart/form-data和application/x-www-form-urlencoded。
為了方便,我們下面就用form-data和x-www-form-urlencoded表示。
兩者的區(qū)別,可謂是老生常談,隨便百度一下,也是有大堆資料??墒俏疫€想用一篇文章來總結(jié)一下,主要有兩點(diǎn)原因:
- form-data和x-www-form-urlencoded雖然是基礎(chǔ),但卻很重要。而且最近在工作中,恰好遇到了這方便的坑。經(jīng)過一番研究,有了新的體悟,所以想要總結(jié)一下。
- 文章內(nèi)容不只是比較兩個(gè)的區(qū)別,還會(huì)引申到HttpServletRequest,以及流不可重復(fù)讀等問題,幫助大家把相關(guān)的知識(shí)串起來。
好了,閑話少敘,我們進(jìn)入正題。
二、form-data和x-www-form-urlencoded區(qū)別
form-data 和 x-www-form-urlencoded 都是表單請(qǐng)求的一種格式,主要區(qū)別有兩點(diǎn)。
編碼方式不同
我們先說x-www-form-urlencoded,它的編碼方式就隱藏在名字里:urlencoded??吹竭@個(gè)關(guān)鍵詞,你想到了什么?
沒錯(cuò),就是js中的encodeURI函數(shù),這個(gè)函數(shù)的功能,我相信大家并不陌生,我們就不在贅述了。
x-www-form-urlencoded就可以理解為被encodeURI編碼過的querystring。
比如,我們用x-www-form-urlencoded提交如下內(nèi)容:
name: 張三
age: 18
實(shí)際上,請(qǐng)求體會(huì)被編碼成如下格式:
name=%E5%BC%A0%E4%B8%89&age=18
這里需要強(qiáng)調(diào)一下,urlencoded這種格式并不是json格式,因此不能使用json庫將其轉(zhuǎn)成json。
接下里,我們來講form-data,完整的表示為multipart/form-data。
multipart/form-data這種格式,會(huì)把表單內(nèi)容分成多個(gè)部分,這也就是multipart的含義。
比如,同樣是上面的表單內(nèi)容,使用multipart/form-data格式,請(qǐng)求體將會(huì)類似下面這樣:
--AaB03x Content-Disposition: form-data; name="name" Content-Type: text/plain 張三 --AaB03x Content-Disposition: form-data; name="age" Content-Type: text/plain 18 --AaB03x--
接下來,我們?cè)敿?xì)說一下其中的細(xì)節(jié)。
- --AaB03x,boundary,也就是邊界,它就是分割表單不同part的分界線。AaB03x是一個(gè)隨機(jī)字符串,需要保證整個(gè)請(qǐng)求體都是用相同的boundary。
- name="name"和name="age",這個(gè)很容易理解,就是每個(gè)part的名字,也就是字段名。
- Content-Type,每個(gè)part的內(nèi)容類型,我們這里因?yàn)閭鞯氖瞧胀ㄎ谋?,所以?nèi)容類型使用text/plain。
- --boundary表示一個(gè)part的開始,--boundary--表示所有part都結(jié)束了。
除了上面的請(qǐng)求體,我們的請(qǐng)求頭需要向下面這樣設(shè)置:
Content-Type: multipart/form-data; boundary=AaB03x
這里關(guān)鍵的部分就是boundary=AaB03x,定義分界線的隨機(jī)數(shù)是什么,請(qǐng)求體中使用的隨機(jī)數(shù),需要和請(qǐng)求頭中定義的隨機(jī)數(shù)一致。
好了,這兩種編碼方式的基本格式,我們已經(jīng)講完了,你的第一感覺是什么,是不是覺得multipart/form-data的格式,要比x-www-form-urlencoded復(fù)雜的多。
沒錯(cuò),這正x-www-form-urlencoded的一大優(yōu)點(diǎn)——對(duì)于普通的文本內(nèi)容,x-www-form-urlencoded占用的字節(jié)更少。
支持的內(nèi)容類型不同
x-www-form-urlencoded只支持普通的文本內(nèi)容,而multipart/form-data的每一個(gè)part部分,都支持不同的Content-Type,比如圖片、音頻、視頻等。
比如,我們向表單里面添加一個(gè)圖片:
--AaB03x Content-Disposition: form-data; name="name" Content-Type: text/plain 張三 --AaB03x Content-Disposition: form-data; name="age" Content-Type: text/plain 18 --AaB03x Content-Disposition: form-data; name="age"; filename="test.jpg" Content-Type: image/jpeg xxxxxxxx --AaB03x--
和普通的文本內(nèi)容不同的是,我們?cè)黾右粋€(gè)filename參數(shù),來表示文件名稱;并且把Content-Type設(shè)置成image/jpeg,表明它的格式是圖片。
而且,你肯定也意識(shí)到了,multipart/form-data也不是json格式,也不能用json庫讀取它。
接下來,我們簡(jiǎn)單總結(jié)一下兩者區(qū)別。
- x-www-form-urlencoded,一種輕型表單,只支持普通文本,優(yōu)點(diǎn)是占用字節(jié)少。
- multipart/form-data,會(huì)把內(nèi)容分成多個(gè)部分,每個(gè)部分都支持不同的格式,優(yōu)點(diǎn)是支持文件上傳,缺點(diǎn)是占用字節(jié)多。
三、HttpServeltRequest的getParameter
上面,我們已經(jīng)把multipart/form-data和x-www-form-urlencoded的基本區(qū)別,給大家講明白了。
接下來,我們來延伸一個(gè)內(nèi)容,就是HttpServletRequest的getParameter——這個(gè)我們幾乎每天都會(huì)接觸到的方法。
我之前一直以為,getParameter方法只能獲取請(qǐng)求地址上的參數(shù),比如:
/api/user?name=張三&age=18
直到最近,我才發(fā)現(xiàn)事情遠(yuǎn)沒有這么簡(jiǎn)單。這也是我為什么要單獨(dú)給大家引申這塊內(nèi)容的原因。
實(shí)際上,getParameter除了能獲取請(qǐng)求地址上的參數(shù),還能獲取表單中的參數(shù),包括x-www-form-urlencoded和form-data這兩種格式。
不知道,你有沒有好奇過,為什么getParameterMap返回的是Map<String, String[]>格式,除了請(qǐng)求地址上可能傳數(shù)組的情況,比如:
/api/user?name=張三&age=18&age=20
另外一個(gè)重要的原因,就是同一個(gè)參數(shù)名,除了可以出現(xiàn)在url地址上,還可以出現(xiàn)在請(qǐng)求體中,比如下面的post請(qǐng)求:
/api/user?name=張三&age=18
Content-Type: application/x-www-form-urlencoded
age=20&gender=male
此時(shí)age就有兩個(gè)值,所以返回的是數(shù)組,只是getParameter方法默認(rèn)只返回?cái)?shù)組的第一個(gè)元素而已。
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; } }
至此,你有沒有發(fā)現(xiàn)什么細(xì)思極恐的事情。在之前我一直排斥使用getParameterMap方法,原因就是我討厭處理數(shù)組。
相反,我更情愿使用getParameter。但現(xiàn)在看來,這種便利可能給我的代碼帶來bug——因?yàn)間etParameter并不兼容參數(shù)傳數(shù)組的情況。
當(dāng)然,并不是說getParameter不能用,而是在用之前需要問下自己,這個(gè)參數(shù)是不是只會(huì)傳單個(gè)值。
這里還能總結(jié)一個(gè)經(jīng)驗(yàn):如果可以,不要用HttpServletRequest接收參數(shù),更好的做法是將請(qǐng)求參數(shù)封裝成一個(gè)類。
比如:
@Data public class UserDTO { private String name; private List<Integer> age; }
一方面,數(shù)據(jù)類型能夠傳達(dá)更多的信息,告訴我們這個(gè)參數(shù)可能傳遞一個(gè)數(shù)組;另一方便,Spring的data-binder能幫我處理好一切。
雖然,getParameter可以獲取x-www-form-urlencoded和form-data中參數(shù),但是它們還是有一些細(xì)微差別。
- x-www-form-urlencoded:所有的參數(shù)都能被獲取
- multipart/form-data:不能讀取帶有filename標(biāo)記的內(nèi)容
private void parseRequest(HttpServletRequest request) { try { Collection<Part> parts = request.getParts(); this.multipartParameterNames = new LinkedHashSet<>(parts.size()); MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); ContentDisposition disposition = ContentDisposition.parse(headerValue); String filename = disposition.getFilename(); if (filename != null) { if (filename.startsWith("=?") && filename.endsWith("?=")) { filename = MimeDelegate.decode(filename); } files.add(part.getName(), new StandardMultipartFile(part, filename)); } else { // 只有filename為空part,才會(huì)放到multipartParameterNames里面 this.multipartParameterNames.add(part.getName()); } } setMultipartFiles(files); } catch (Throwable ex) { handleParseFailure(ex); } }
你是不是還好奇,getParameter取數(shù)組的第一個(gè)元素,到底是請(qǐng)求體中的參數(shù),還是url地址上的參數(shù)呢。
在寫這篇文章時(shí),我本打算介紹一下這個(gè)細(xì)節(jié)。但是后來,理智告訴我放棄傳播這種無用的知識(shí),因?yàn)樗⒉荒苤笇?dǎo)我們寫出更好的代碼。相反可能會(huì)將我們引入錯(cuò)誤的方向,比如下面的代碼:
Map<String, String[]> parameterMap = request.getParameterMap(); if (parameterMap.containsKey("age") && parameterMap.get("age").length > 1) { String age = parameterMap.get("age")[1]; }
你知道它想表達(dá)什么嗎?
好了,這一節(jié)的內(nèi)容細(xì)節(jié)有點(diǎn)多,如果你讀起來有點(diǎn)吃力,建議多讀幾遍。
四、流不可重復(fù)讀
流不可重復(fù)讀的問題,相信你一定遇到過。不過,你可能會(huì)疑問:我們今天講的內(nèi)容,跟這個(gè)問題有什么關(guān)系呢?
你別說,還真有,我最近就被這個(gè)問題坑了!
最近,我在工作中要開發(fā)一個(gè)接口代理的工具,需要對(duì)請(qǐng)求體進(jìn)行一些處理。當(dāng)我測(cè)試x-www-form-urlencode格式時(shí),死活獲取不到請(qǐng)求體。通過debug才發(fā)現(xiàn),原來是getParameter搞的鬼。
相信通過前面的介紹,你已經(jīng)知道原因:對(duì)于form-data和x-www-form-urlencoded這兩種格式,getParameter方法會(huì)從請(qǐng)求體中查詢參數(shù),既然是請(qǐng)求體,當(dāng)然會(huì)讀取InputStream,從而導(dǎo)致后續(xù)再讀取流的時(shí)候獲取不到任何內(nèi)容。
這個(gè)問題的一般解法,相信你已經(jīng)知道了,就是寫一個(gè)Filter對(duì)HttpServletRequest包裝,讀取InputStream中的數(shù)據(jù),并緩存到一個(gè)byte[]中。包裝類重寫getInputStream方法返回一個(gè)ByteArrayInputStream,從而解決流不同重復(fù)讀的問題。
上面的解法,網(wǎng)上的資料一搜一大堆,我就不貼代碼了。
我今天要給你介紹的是另外一種思路,這種思路只能部分解決由于getParameter或getParameterMap導(dǎo)致的流讀取的問題。
這里的部分解決,指的是如果你只是希望獲取請(qǐng)求地址上的參數(shù),那么這個(gè)方法能夠奏效——它不會(huì)讀取IO流。
這里的思路就是解析querystring。
可以通過request.getQueryString方法獲取查詢字符串,接下來的關(guān)鍵,就是如何方便的處理它。
推薦兩個(gè)工具類,第一個(gè)是UriComponentsBuilder:
String queryString = request.getQueryString(); MultiValueMap<String, String> queryParams = UriComponentsBuilder.fromUriString("?" + queryString) .build() .getQueryParams();
這個(gè)類是Spring提供的,對(duì)于SpringBoot用戶來說,開箱即用。
第二個(gè)是URLEncodedUtils:
String queryString = request.getQueryString(); List<NameValuePair> nameValuePairs = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8); Map<String, List<String>> queryParams = nameValuePairs.stream() .collect(Collectors.groupingBy(NameValuePair::getName)) .entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream() .map(NameValuePair::getValue).collect(Collectors.toList())));
這個(gè)類是httpclient提供的工具類,需要引入maven包:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>${version}</version> </dependency>
這兩個(gè)工具類,能很好的幫助我們處理querystring,別再傻傻的用split函數(shù)分割了。
五、總結(jié)
“溫故而知新,可以為師矣”。
這篇文章從form-data和x-www-form-urlencoded的區(qū)別為起點(diǎn),一步步展開,介紹了HttpServletRequest的getParameter方法,及其導(dǎo)致的流不可重復(fù)讀的問題。
到此這篇關(guān)于form-data與x-www-form-urlencoded的區(qū)別以及知識(shí)延伸的文章就介紹到這了,更多相關(guān)form-data與x-www-form-urlencoded區(qū)別內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Elasticsearch封裝公共索引增刪改查
索引是Elasticsearch中存儲(chǔ)數(shù)據(jù)的邏輯單元,類似于關(guān)系數(shù)據(jù)庫中的表,它包含多個(gè)文檔,每個(gè)文檔都是一個(gè)結(jié)構(gòu)化的JSON數(shù)據(jù)格式,在實(shí)際應(yīng)用中,索引的使用與配置可以依據(jù)不同的方案進(jìn)行,例如在Spring Boot項(xiàng)目中,可以選擇自動(dòng)配置或者手動(dòng)編寫配置類2024-10-10SpringValidation自定義注解及分組校驗(yàn)功能詳解
這篇文章主要介紹了SpringValidation自定義注解及分組校驗(yàn)功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01SpringBoot中的@EnableAutoConfiguration注解解析
這篇文章主要介紹了SpringBoot中的@EnableAutoConfiguration注解解析,@EnableAutoConfiguration也是借助@Import的幫助,將所有符合自動(dòng)配置條件的bean定義注冊(cè)到IoC容器,需要的朋友可以參考下2023-09-09SpringBoot之spring.factories的使用方式
這篇文章主要介紹了SpringBoot之spring.factories的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01一文搞懂Java常見的三種代理模式(靜態(tài)代理、動(dòng)態(tài)代理和cglib代理)
Java中常見的三種代理模式是靜態(tài)代理模式、動(dòng)態(tài)代理模式和CGLIB代理模式,本文就來給大家詳細(xì)的講解一下這三種代理模式,感興趣的小伙伴跟著小編一起來看看吧2023-08-08mybatis-plus?插入修改配置默認(rèn)值的實(shí)現(xiàn)方式
這篇文章主要介紹了mybatis-plus?插入修改配置默認(rèn)值的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07