四种常见的POST提交数据⽅式专题
定义和⽤法
enctype 属性规定在发送到服务器之前应该如何对表单数据进⾏编码。
默认地,表单数据会编码为 "application/x-www-form-urlencoded"。就是说,在发送到服务器之前,所有字符都会进⾏编码(空格转换为 "+" 加号,特殊符号转换为 ASCII HEX 值)。
enctype属性值
值描述
application/x-www-form-urlencoded在发送前编码所有字符(默认)
不对字符编码。
multipart/form-data
在使⽤包含⽂件上传控件的表单时,必须使⽤该值。
text/plain空格转换为 "+" 加号,但不对特殊字符编码。
规定的 HTTP 请求⽅法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这⼏种。
其中 POST ⼀般⽤来向服务端提交数据,本⽂主要讨论 POST 提交数据的⼏种⽅式。
我们知道,HTTP 协议是以 ASCII 码传输,建⽴在 TCP/IP 协议之上的应⽤层规范。
规范把 HTTP 请求分为三个部分:状态⾏、请求头、消息主体。类似于下⾯这样:
BASH<method> <request-URL> <version>
<headers>
<entity-body>
协议规定 POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使⽤什么编码⽅式。实际上,开发者完全可以⾃⼰决定消息主体的格式,只要最后发送的 HTTP 请求满⾜上⾯的格式就可以。
但是,数据发送出去,还要服务端解析成功才有意义。⼀般服务端语⾔如 php、python 等,以及它们的 framework,都内置了⾃动解析常见数据格式的功能。
服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是⽤何种⽅式编码,再对主体进⾏解析。
所以说到 POST 提交数据⽅案,包含了 Content-Type 和消息主体编码⽅式两部分。
下⾯就正式开始介绍它们。
application/x-www-form-urlencoded
这应该是最常见的 POST 提交数据的⽅式了。浏览器的原⽣ <form> 表单,如果不设置enctype属性,那么最终就会以 application/x-www-form-urlencoded ⽅式提交数据。
<form action="form_action.asp" enctype="text/plain">
<p>First name: <input type="text" name="fname" /></p>
<p>Last name: <input type="text" name="lname" /></p>
<input type="submit" value="Submit" />
</form>
此时Form提交的请求数据,抓包时看到的请求会是这样的内容(⽆关的请求头在本⽂中都省略掉了):
BASHPOST ample HTTP/1.1 Content-Type: application/x-www-form-urlencoded;charset=utf-
8 title=test&sub%5B%5D=1&sub%5B%5D=2&sub%5B%5D=3
⾸先,Content-Type 被指定为 application/x-www-form-urlencoded;其次,提交的数据按照 key1=val1&key2=val2 的⽅式进⾏编码,key 和 val 都进⾏了 URL 转码。
⼤部分服务端语⾔都对这种⽅式有很好的⽀持。例如 PHP 中,$_POST['title'] 可以获取到 title 的值,$_POST['sub'] 可以得到 sub 数组。
很多时候,我们⽤ Ajax 提交数据时,也是使⽤这种⽅式。
例如和的 Ajax,Content-Type 默认值都是「application/x-www-form-urlencoded;charset=utf-8」。
multipart/form-data
这⼜是⼀个常见的 POST 数据提交的⽅式。我们使⽤表单上传⽂件时,必须让 <form> 表单的enctype 等于 multipart/form-data。直接来看⼀个请求⽰例:
BASHPOST ample HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
这个例⼦稍微复杂点。⾸先⽣成了⼀个 boundary ⽤于分割不同的字段,为了避免与正⽂内容重复,boundary 很长很复杂。然后 Content-Type ⾥指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体⾥按照字段个数⼜分为多个结构类似的部分,每部分都是以--boundary开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(⽂本或⼆进制)。如果传输的是⽂件,还要包含⽂件名和⽂件类型信息。消息主体最后以--boundary--标⽰结束。
关于 multipart/form-data 的详细定义,请前往查看。
这种⽅式⼀般⽤来上传⽂件,各⼤服务端语⾔对它也有着良好的⽀持。
上⾯提到的这两种 POST 数据的⽅式,都是浏览器原⽣⽀持的,⽽且现阶段标准中原⽣ <form> 表单也(通过 <form> 元素的enctype属性指定,默认为application/x-www-form-urlencoded。其实enctype还⽀持text/plain,不过⽤得⾮常少)。
随着越来越多的 Web 站点,尤其是 WebApp,全部使⽤ Ajax 进⾏数据交互之后,我们完全可以定义新的数据提交⽅式,给开发带来更多便利。
application/json
application/json 这个 Content-Type 作为响应头⼤家肯定不陌⽣。实际上,现在越来越多的⼈把它作
为请求头,⽤来告诉服务端消息主体是序列化后的 JSON 字符串。由于JSON 规范的流⾏,除了低版本 IE 之外的各⼤浏览器都原⽣⽀持 JSON.stringify,服务端语⾔也都有处理 JSON 的函数,使⽤ JSON 不会遇上什么⿇烦。
JSON 格式⽀持⽐键值对复杂得多的结构化数据,这⼀点也很有⽤。记得我⼏年前做⼀个项⽬时,需要提交的数据层次⾮常深,我就是把数据 JSON 序列化之后来提交的。不过当时我是把 JSON 字符串作为 val,仍然放在键值对⾥,以 x-www-form-urlencoded ⽅式提交。
Google 的中的 Ajax 功能,默认就是提交 JSON 字符串。例如下⾯这段代码:
JSvar data = {'title':'test', 'sub' : [1,2,3]};
$http.post(url, data).success(function(result) {
...
});
最终发送的请求是:
BASHPOST ample HTTP/1.1
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}
这种⽅案,可以⽅便的提交复杂的结构化数据,特别适合 RESTful 的接⼝。各⼤抓包⼯具如 Chrome ⾃带的开发者⼯具、Firebug、Fiddler,都会以树形结构展⽰ JSON 数据,⾮常友好。但也有些服务端语⾔还没有⽀持这种⽅式,例如 php 就⽆法通过 $_POST 对象从上⾯的请求中获得内容。这时候,需要⾃⼰动⼿处理下:在请求头中 Content-Type 为 application/json 时,从php://input⾥获得原始输⼊流,再json_decode成对象。⼀些 php 框架已经开始这么做了。
当然 AngularJS 也可以配置为使⽤ x-www-form-urlencoded ⽅式提交数据。如有需要,可以参考。
text/xml
我的博客之前(XML Remote Procedure Call)。它是⼀种使⽤ HTTP 作为传输协议,XML 作为编码⽅式的远程调⽤规范。典型的 XML-RPC 请求是这样的:
HTMLPOST ample HTTP/1.1
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
<methodName&StateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
XML-RPC 协议简单、功能够⽤,各种语⾔的实现都有。它的使⽤也很⼴泛,如 WordPress 的,搜索引擎的等等。JavaScript 中,也有⽀持以这种⽅式进⾏数据交互,能很好的⽀持已有的 XML-RPC 服务。不过,我个⼈觉得 XML 结构还是过于臃肿,⼀般场景⽤ JSON 会更灵活⽅便。
最近为项⽬组提供rest api 时遇到了关于接⼝参数的传递问题,主要是没有充分考虑到第三⽅调⽤者的使⽤⽅式,应该尽量的去兼容公司之前提供出去的接⼝调⽤⽅式,这样可以降低第三⽅调⽤者的学习成本,尽管之前的⽅式并不是那么的推荐,好的做法是即兼容⽼的做法也⽀持推荐的做法。
对于基于http post接⼝,Content-type我会优先选择application/json,但公司之前提供的接⼝恰恰采⽤了application/x-www-form-urlencoded,它是表单默认的提交类型,基于key/value形式提交到服务端的。
spring mvc是如何接收下⾯两种经典数据的? (⾄于form-data,它即可以传键值对也可以上传⽂件,这⾥不涉及到⽂件所以只讨论下⾯两种):Content-type=application/json:需要在参数上增加@RequestBody这个注解,说明参数是从http的requestbody中获取。
下图中的参数,是标准的json格式,对前端js⾮常友好。
Content-type=application/x-www-form-urlencoded,参数上不能增加@RequestBody的注解
下图的可以看出参数形式与get请求时,URL后⾯的参数格式
为什么不推荐采⽤application/x-www-form-urlencoded这种类型,它有如下问题:
测试困难,就别想通过postman这类⼯具测试:提交到服务端实际上是⼀个MultiValueMap(org.springframework.util.MultiValueMap或
org.springframework.util.LinkedMultiValueMap),
如果value中的对象也是⼀个对象,那么在构建这个参数时就⾮常困难,看下它的过程
采⽤key1=value1&key2=value2这种形式将所有参数拼接起来,从⼀长串字符中想了解每个参数的含义没有个好眼⼒怕是不⾏。
value要进⾏编码,编码之后的对调试者不友好。
value是复杂对象的情况更加糟糕,⼀般只能通过程序来序列化得到参数,想⼿写基本不可能。
客户端调⽤复杂
需要去构建List<NameValuePair>,⼀般页⾯传递的参数都是⼀个实体对象Model,需要额外的将这个Model转换成List<NameValuePair>,如果这个对象复杂,那么构建这个Key/Value就够⼈烦的了。
这⾥给⼀个java通过apache httpclient调⽤的对⽐,看看哪⼀个简单。
application/x-www-form-urlencoded
需要⼿⼯将model转换成NameValuePair。
application/json
这⾥只需要Model即可,不需要⼆次转换,结构也⾮常清楚。
key/value的语⾔表达形式没有json强,下⾯两种你更加喜欢哪⼀个呢?
字符串
post man这类模似http请求的⼯具中,如果key对应的value是个对象,那么你需要通过⼯具得到它的序列化之后的字符串然后填写到字段中,想想都烦。如果你说我不需要通过这些模似⼯具测试,那就另当别论
json
数据结构更加复杂
如果需要提交的对象⾮常复杂,属性⾮常多,如果将所有的属性都构建到MultiValueMap中,那个Map的构建会⾮常复杂,试想如果对象有多级嵌套对象呢。所有为了避免这个问题,我们将需要提交的业务对象做为⼀个key来存储,value就是对象序列化之后的字符串。再加了⼀些⾮业务参数,⽐如安全⽅⾯的token等参数,有效的降低了MultiValueMap构建的复杂度。但这种⽅式相对于json的传递⽅式来讲层次更深。如下图,我们的参数多了⼀层,jsonParam。
如果解决呢?
不能不兼容现有的模式,但⼜想⽀持json,焦点就是在参数的接收上,让其能够完美的兼容上述两种参数传递,这⾥可以从HttpMessageConverter着⼿,这个就是⽤来将请求的参数映射到spring mvc⽅法中的实体参数的。我们可以编写⼀个⾃定义的类,内部借⽤FormHttpMessageConverter来接收MultiValueMap,即使⽅法参数上增加了js获取json的key和value
@RequestBody的注解,也会⾛我们⾃定义的converter,就有机会去重新给参数赋值。
这个⽅法中需要解决⼀个问题,就是客户端传递时每个参数都是当成字符串来处理的,这种导致我们通过FormHtppMessageConverter转换成Map时,原本是对象的属性被识别成字符串,⽽不是object,结果就是在反序列化时会出错。好在,上⾯我们将需要提交的对象包装了⼀次,产⽣⼀个公共的object参数jsonParam,只需要处理这⼀个特殊对象。做法就是从Map取出jsonParam,然后对其内容进⾏反序列化,更新Map值,再次进⾏反序列化就正常了。
上图中的做法⽬前有如下问题
序列化的字段是约定好的,也是基于我们的post model基本上来处理的,是针对性的converter
代码最后⾯调⽤的jackon的convertValue,对需要反序列化的对象类型有要求,好像不⽀持泛型类型,⽐如这种类型的就不⾏:
CommonParamInfoDto<SearchParamInfo<ProductSearchInfo>>
完整的conveter代码如下,其实主要代码就是上图贴图中的那么对特定字段的序列化处理,其它的⽅法都是默认即可。
public class ObjectHttpMessageConverter implements HttpMessageConverter<Object> {
private final FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
private final ObjectMapper objectMapper = new ObjectMapper();
private static final LinkedMultiValueMap<String, ?> LINKED_MULTI_VALUE_MAP = new LinkedMultiValueMap<>();
private static final Class<? extends MultiValueMap<String, ?>> LINKED_MULTI_VALUE_MAP_CLASS
= (Class<? extends MultiValueMap<String, ?>>) LINKED_MULTI_Class();
@Override
public boolean canRead(Class clazz, MediaType mediaType) {
return objectMapper.canSerialize(clazz) && formHttpMessageConverter.canRead(MultiValueMap.class, mediaType);
}
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
SupportedMediaTypes();
}
@Override
public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
Map input = ad(LINKED_MULTI_VALUE_MAP_CLASS, inputMessage).toSingleValueMap();
String jsonParamKey="jsonParam";
ainsKey(jsonParamKey)) {
String jsonParam = (jsonParamKey).toString();
SearchParamInfo<Object> searchParamInfo = new SearchParamInfo<Object>();
Object jsonParamObj = JsonHelper.json2Object(jsonParam, Class());
input.put("jsonParam", jsonParamObj);
}
Object objResult= vertValue(input, clazz);
return objResult;
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws UnsupportedOperationException {
throw new UnsupportedOperationException("");
}
}
配置,写好了conveter之后,需要在配置⽂件中配置上才能⽣效。
最后,我们的⽅法就可以这样写,即可以⽀持 key/value对,也⽀持json
我的⽬的在于api的参数即能⽀持application/x-www-form-urlencoded也能⽀持application/json,上⾯是我⽬前能想到的办法,如果⼤家有其它更好的办法多多指点。
根据上⾯的描述,可以得到以下结论:
对象将转换成什么样的内容类型很⼤程序上取决于传递给put⽅法的类型,
如果给定⼀个String值,那么将会使⽤StringHttpMessageConverter,这个值直接被写到请求体中,内容类型设置为"text/plain"
如果给定⼀个MultiValueMap<String,String>,那么这个Map中的值会被FormHttpMessageConverter以“application/x-www-form-urlencoded”的格式写到请求体中
因为给定的是⼀个⾃定义对象,所以需要⼀个能够处理任意对象的信息转换器。如果在类路径下包含Jackson2库,那么MappingJacksonHttpMessageConverter将以application/json格式将⾃定义对象写到请求中
我⼜可以愉快的使⽤post man测试了。⽽且可以推荐第三⽅调⽤者优先使⽤json,我相信这种即能简化编程⼜⽅便调试的优点应该能够吸引它们。
postman中 form-data、x-www-form-urlencoded、raw、binary的区别
1、form-data:
就是http请求中的multipart/form-data,它会将表单的数据处理为⼀条消息,以标签为单元,⽤分隔符分开。既可以上传键值对,也可以上传⽂件。当上传的字段是⽂件时,会有Content-Type来表名⽂件类型;content-disposition,⽤来说明字段的⼀些信息;
由于有boundary隔离,所以multipart/form-data既可以上传⽂件,也可以上传键值对,它采⽤了键值对的⽅式,所以可以上传多个⽂件。
新版本Postman在此处查看:
2、x-www-form-urlencoded:
就是application/x-www-from-urlencoded,会将表单内的数据转换为键值对,⽐如,name=java&age = 23
3、raw
可以上传任意格式的⽂本,可以上传text、json、xml、html等
4、binary
相当于Content-Type:application/octet-stream,从字⾯意思得知,只可以上传⼆进制数据,通常⽤来上传⽂件,由于没有键值,所以,⼀次只能上传⼀个⽂件。
multipart/form-data与x-www-form-urlencoded区别
multipart/form-data:既可以上传⽂件等⼆进制数据,也可以上传表单键值对,只是最后会转化为⼀条信息;
x-www-form-urlencoded:只能上传键值对,并且键值对都是间隔分开的。
上传⽂件的表单中<form>要加属性enctype="multipart/form-data",很多⼈只是死记硬背知道上传表单要这么写,知其然⽽不知其所以然。那到底为什么要添加这个属性呢?它是什么意思呢?它⼜有什么其他可选值呢?其实form表单在你不写enctype属性时,也默认为其添加了enctype属性值,默认值是enctype="application/x- www-form-urlencoded".这个属性管理的是表单的MIME编码,共有三个值可选:
①application/x-www-form-urlencoded (默认值)
②multipart/form-data
③text/plain
其中①application/x-www-form-urlencoded是默认值,⼤家可能在AJAX⾥见过这个:xmlHttp.setRequestHeader("Content-Type","application/x-www-form- urlencoded"); 这两个要做的是同⼀件事情,就是设置表单传输的编码。在AJAX⾥不写有可能会报错,但是在HTML的form表单⾥是可以不写 enctype="application/x-www-form-urlencoded"的,因为默认HTML表单就是这种传输编码类型。⽽②multipart-form-data是⽤来指定传输数据的特殊类型的,主要就是我们上传的⾮⽂本的内容,⽐如图⽚或者mp3等等。
③text/plain是纯⽂本传输的意思,在发送邮件时要设置这种编码类型,否则会出现接收时编码混乱的问题,⽹络上经常拿text/plain和 text/html做⽐较,其实这两个很好区分,前者⽤来传输纯⽂本⽂件,后者则是传递html代码的编码类型,在发送头⽂件时才⽤得上。①和③都不能⽤于上传⽂件,只有multipart/form-data才能完整的传递⽂件数据。
上⾯提到的MIME,它的英⽂全称是"Multipurpose Internet Mail Extensions" 多功能Internet 邮件扩充服务,它是⼀种多⽤途⽹际邮件扩充协议,在1992年最早应⽤于电⼦邮件系统,但后来也应⽤到浏览器。服务器会将它们发送的多媒体数据的类型告诉浏览器,⽽通知⼿段就是说明该多媒体数据的MIME类型,从⽽让浏览器知道接收到的信息哪些是MP3⽂件,哪些是Shockwave⽂件等等。服务器将 MIME标志符放⼊传送的数据中来告诉浏览器使⽤哪种插件读取相关⽂件。
简单说,MIME类型就是设定某种扩展名的⽂件⽤⼀种应⽤程序来打开的⽅式类型,当该扩展名⽂件被访问的时候,浏览器会⾃动使⽤指定应⽤程序来打开。多⽤于指定⼀些客户端⾃定义的⽂件名,以及⼀些媒体⽂件打开⽅式。
浏览器接收到⽂件后,会进⼊插件系统进⾏查,查出哪种插件可以识别读取接收到的⽂件。如果浏览器不清楚调⽤哪种插件系统,它可能会告诉⽤户缺少某插件,或者直接选择某现有插件来试图读取接收到的⽂件,后者可能会导致系统的崩溃。传输的信息中缺少MIME标识可能导致的情况很难估计,因为某些计算机系统可能不会出现什么故障,但某些计算机可能就会因此⽽崩溃。
检查⼀个服务器是否正确设置了MIME类型的步骤是:
1. 在Netscape浏览器中打开服务器⽹页
2. 进⼊"View"菜单,选择"Page Info"
3. 在弹出的窗⼝中点击上层框架中的"EMBED"
4. 在下层框架中查看MIME的类型是否为"application/x-director"或"application/x-shockwave- flash",如果是上述信息的话表明服务器已经正确设置了MIME类型;
⽽如果MIME类型列出的是⽂本内容、⼋位⼀组的数据或是其它形式均表明服务器的MIME类型没有设置正确。
如果服务器没有正确标明其发送的数据的类型,服务器管理员应该正确添加相关信息,具体操作⽅法⾮常简单快捷。
每个MIME类型由两部分组成,前⾯是数据的⼤类别,例如声⾳audio、图象image等,后⾯定义具体的种类。
常见的MIME类型
超⽂本标记语⾔⽂本 .html,.html text/html
普通⽂本 .txt text/plain
RTF⽂本 .rtf application/rtf
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论