multipartform-data与httpclient⽂件上传
写在前⾯:本⽂讨论的内容都是基于java相关技术栈。
⽂件上传⽆论是在传统的基于html的web系统开发,还是⽬前主流的移动app开发,都是⼀个⽐较常见的功能需求。例如:web oa系统,可能会涉及到各种⽂档、合同、档案⽂件的上传。移动app的开发可能会涉及到⽤户头像、图⽚动态、语⾳动态、视频动态等多媒体⽂件的上传。但是传统的html web⽂件上传和移动app开发中的⽂件上传的技术实现还是有很⼤的差异的。
本⽂重点讨论的是httpclient⽅式进⾏⽂件上传,但并不是只贴实现代码,⽽是希望通过循序渐进的⽅式能够让⼤家理解为什么httpclient⽂件上传要那么写。
所以本⽂的表达脉络是先通过html web的⽂件上传来了解http协议,了解了⽂件上传相关的http协议后,再去理解httpclient⽂件上传的代码实现,以及前端的httpclient⽂件上传代码与后端的springmvc服务代码如何配合。
html web⽂件上传
在进⾏java web系统开发的时候,我们要实现⽂件上传,最简单的⽅式应该就是通过html的form表单提交,如果要通过form表单进⾏⽂件上传的话,有两个要点:
1、 form表单中包含input file元素
<input type="file"name="filename"/>
2、将form表单的enctype属性设置为multipart/form-data。
<form action="xxxxx/xxxx"method="post"enctype="multipart/form-data">
下⾯来看下⼀段完整的html⽂件上传代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>⽂件上传demo</title>
</head>
<body>
<form action="192.168.111.11/api/file/upload"method="post"enctype="multipart/form-data">
<div>
<textarea name="moment"placeholder="说点什么..."></textarea>
</div>
<div>
<input type="file"name="file0"/>
</div>
<div>
<input type="file"name="file1"/>
</div>
<div>
<input type="file"name="file2"/>
</div>
<div>
<input type="submit"/>
</div>
</form>
</body>
</html>
运⾏后的效果如下:
表单内容并不多,模拟⼀个类似发朋友圈的⼩功能。可以输⼊⽂字,可以上传图⽚(图⽚没后做强校验,也可以上传⽂本⽂件,主要为了观察报⽂,最多上传三个⽂件)。点击提交之后,就会请求⼀个url,这url可以随便写⼀个,我们主要是为了观察请求报⽂。
录⼊了⽂本、选择了⼀个⽂本⽂件、⼀个图⽚⽂件。
点击提交按钮后,⽤charles抓取了该提交的http请求报⽂如下:
http协议报⽂解析
如上图所⽰,我们主要来分析报⽂的主要框架和相关属性做⼀些说明,其余的很多细节属性本⽂不做展开。
请求报⽂
请求报⽂由3部分组成
请求⾏
"post"是请求⽅法。GET和POST是最常见的HTTP⽅法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。不过,当前的⼤多数浏览器只⽀持GET和POST
post后⾯紧跟的是请求路径。
"HTTP/1.1"是http协议的版本。
请求头:请求头⾥会包含很多属性,例如:
host指明了请求的主机地址和端⼝号。
Content-length指明了发送的报⽂长度。
Content-type指定了请求报⽂体的MIME类型。(这是我们重点关注的部分)
请求体:这⾥⾯就是发送的具体内容了。不同的Content-type对应的请求提的内容格式也是不同的。
如果Content-type为text/plain,那么请求体的内容就应该是纯⽂本的字符串。
如果Content-type为application/json,那么请求体的内容就应该是json格式的字符串。
如果Content-type为multipart/form-data,那么请求体的内容就是另外⼀中相对较复杂的结构化的格式。(下⾯介绍)
请求头和请求体这两部分内容之间是通过⼀个空⾏来分隔的
响应报⽂
请求报⽂由3部分组成
状态⾏
"HTTP/1.1"是http协议的版本。
"200"是http响应状态码。
"OK"是状态码的描述。
响应头:有些header属性是响应头和请求头都可以使⽤的:
Content-type指定了响应报⽂体的MIME类型。
响应体:这⾥⾯就是响应的具体内容了。不同的Content-type对应的响应体的内容格式也应该是不同的(不绝对)。
如果Content-type为text/html,那么响应体的内容就应该是html页⾯。
如果Content-type为application/json,那么响应体的内容就应该是json格式的字符串。
如果Content-type为octet-stream,那么响应体的内容就应该是⼆进制流。(常⽤于⽂件下载)
响应头和响应体这两部分内容之间也是通过⼀个空⾏来分隔的
为什么请求头和请求体、响应头和响应体之间⽤空⾏来分隔?
主要是因为http协议的解析规则是以\r\n来区分各个报⽂部分的。
以请求报⽂为例:请求⾏的内容只有⼀⾏,以\r\n结尾。服务端解析时只要读取到第⼀个\r\n,就可以认为请求⾏已经读取完成了,再往下读取的⾏就应该是header的属性了,header会有很多属性,每⼀个属性都是⼀⾏,也同样是以\r\n结尾。那读取到什么时候才能读取到请求体呢,也就是连续读取到2个\r\n的时候,其中后⼀个\r\n也就是那个⽤来分隔的空⾏,这时就意味着请求头已经读取完了,往下再读已经是请求体了。
响应报⽂同理,因为响应报⽂的状态⾏也只有⼀⾏。响应头也会有多⾏。
⽂件上传的请求报⽂特殊性
上⼀部分的内容主要是为了简单的介绍⼀下http协议,在⽂件上传功能上,我们重点关注的是请求报⽂中的请求头⾥的Content-type属性和请求体⾥的内容。下⾯我们把这连部分内容摘出来。
form表单⽂件上传时的Content-type
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryMvt2bmLJuxgWXCUl
⽂件上传时的请求体内容
------WebKitFormBoundaryMvt2bmLJuxgWXCUl
html中提交表单用什么属性Content-Disposition: form-data; name="moment"
⼼情不错的
------WebKitFormBoundaryMvt2bmLJuxgWXCUl
Content-Disposition: form-data; name="file0"; filename="t1.txt"
Content-Type: text/plain
this is a txt file
------WebKitFormBoundaryMvt2bmLJuxgWXCUl
Content-Disposition: form-data; name="file1"; filename="zxy.jpg"
Content-Type: image/jpeg
这⾥是⼀⼤堆zxy.jpg图⽚对应的⼆进制字节
------WebKitFormBoundaryMvt2bmLJuxgWXCUl
Content-Disposition: form-data; name="file2"; filename=""
Content-Type: application/octet-stream
------WebKitFormBoundaryMvt2bmLJuxgWXCUl--
关于multipart/form-data请求报⽂的特殊性
因为请求报⽂中请求头⾥的Content-Type为multipart/form-data,multipart直接翻译的话叫做"多部分",不⽤太纠结这个叫法,形象点理解的话,可以这样认为。这种请求的http请求,他的请求体不是单⼀⼀个内容,⽽是也是有固定结构的多各部分组成的。每个部分也可能是不同的表现类型,⽐如说,第⼀部分是纯⽂本,第⼆部分是图⽚⽂件,第三部分是json串。这有点像是⼤报⽂⾥嵌套了多个⼩报⽂⼀样,但是⼩报⽂⾥⾯没有请求头,也没有那么多的header属性。但是还应该提供⼀个boundary属性来作为多个⼩报⽂的分隔符。画个图理解⼀下。
boundary
对照上图我们来理解下,boundary翻译为中⽂叫做边界、界限、分界限的意思。基于浏览器进⾏提交时,boundary如果不指定浏览器会⾃动⽣成⼀个字符串,multipart中前⾯加两个中横线也就是以"–{boundary}“来分隔每个独⽴的部分。前后分别加两个中横线也就是以”–{boundary}–"来标识整个报⽂的结束。
Content-Disposition
Content-Disposition是http协议中header中规定的⼀个属性。Disposition中⽂意思为:布置、安排。C
ontent-Disposition也就可以理解为,他定义了报⽂的内容要如何展现或者处理。他在http协议中有两种⽤法:
1、⽤在响应报⽂的header中事,⽤来指⽰响应的内容该以何种形式展⽰,是以内联(inline)的形式(即⽹页或者页⾯的⼀部分),还是以附件(attachment)的形式下载并保存到本地。例如:
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename=“filename.jpg”
2、⽤在multipart的报⽂体中时(既可以⽤于请求报⽂、也可以⽤于响应报⽂),他的作⽤就是⽤来定义每个⼩报⽂的属性值。所谓属性,可以粗略认为只有两个:name、filename。
⽤在multipart/form-data中时,他前⾯的写法固定为:
Content-Disposition: form-data;
后⾯紧跟属性定义。例如:
Content-Disposition: form-data; name=“file1”; filename=“zxy.jpg”
⼩结
到此,我们针对⽂件上传的报⽂的格式、以及⼀些必要属性做了解释。这⼀部分内容如果能够理解,那么再看httpclient⽂件上传的代码就都能对号⼊座了。
httpclient⽂件上传
直接贴代码
public class HttpClientUtil {
/**
* ⽂件上传
*
* @param fileServer 接收请求的远程服务地址 (例如:192.168.111.11/api/file/upload)
* @param filesMap 要上传的多个⽂件的k-v信息,k为要给⽂件定义的name属性值,v就是File类型的本地⽂件集合。可以同⼀个name k对应多个⽂件
* @param param 附加的参数信息(例如:⽂件要关联的某个主体id,备注等。不同业务⾃⼰斟酌)
* @param header ⾃定义的header属性
* @return 返回服务端的响应报⽂
*/
public static String postFile(String fileServer, Map<String, List<File>> filesMap, Map<String, String> param, Map<String, String> header){
CloseableHttpResponse response = null;
CloseableHttpClient httpClient = ateDefault();
HttpPost postRequest =new HttpPost(fileServer);
/
/ 如果⾃定义了header属性,则将⾃定义的header属性填充到请求报⽂的header中
if(header != null){
for(Map.Entry<String, String> entry : Set()){
postRequest.Key(), Value());
}
}
// 创建⼀个Multipart构造器
MultipartEntityBuilder builder = ate();
// 设置为浏览器兼容模式(采⽤模拟浏览器提交的⽅式)
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
/*
设置字符编码为utf-8,这个设置只影响的报⽂体中⼩报⽂的header内容的编码,不能影响⼩报⽂体的编码,这个地⽅如果不设置,默认采⽤ASCII⽅式编码。当然如果这个地⽅不设置,也可以在调⽤端⼿动设置header的Content-type属性为"multipart/form-data;charset=utf-8",指定编码。
*/
builder.setCharset(Charset.forName("utf-8"));
// 遍历⽂件map
for(Map.Entry<String, List<File>> entry : Set()){
// 将要上传的每个⽂件都作为multipart的⼀个part
for(File file : Value()){
// 构造⼀个fileBody ,就相当于构造了⼀个⽂件⼩报⽂
FileBody fileBody =new FileBody(file);
// 把上⾯的⼩报⽂添加到multipart中
builder.Key(), fileBody);
}
}
// 如果附加参数不为空,则将每个参数都作为multipart的⼀个part
if(param != null){
for(Map.Entry<String, String> entry : Set()){
/*
构造⼀个stringBody,就相当于构造了⼀个纯⽂本⼩报⽂.
构造⼀个stringBody,就相当于构造了⼀个纯⽂本⼩报⽂.
并且显⽰设置了content-type为text/plain;charset=utf-8。让参数值采⽤utf-8格式编码.
*/
StringBody stringBody =new Value(), ContentType.TEXT_PLAIN.withCharset("utf-8"));
// 把上⾯的⼩报⽂添加到multipart中
builder.Key(), stringBody);
// 也可以采⽤下⾯这⾏写法
// builder.Key(), Value(), ContentType.TEXT_PLAIN.withCharset("utf-8"));
// 不建议下⾯这⾏写法,⼤概率会有乱码问题。这种⽅式将会以ISO_8859_1格式对value进⾏编码
// builder.Key(), Value());
}
}
// 将构造出http请求报⽂体实体对象设置给HttpPost实例,从⽽构造了⼀个完整的报⽂
postRequest.setEntity(builder.build());
return execute(httpClient, postRequest, response);
}
/**
* 该⽅法是为了抽象与所有post请求的公共逻辑
* postFile ⽅法⽤于上传⽂件
* postJson ⽅法⽤于json请求(为节省篇幅,本⽂略去该⽅法的实现)
* postParam ⽅法⽤于参数键值对请求(为节省篇幅,本⽂略去该⽅法的实现)
*/
private static String execute(CloseableHttpClient client, HttpPost post, CloseableHttpResponse response){
try{
response = ute(post);
HttpEntity entity = Entity();
if(entity != null){
String(entity);
}
}catch(IOException e){
throw new Message(), e);
}finally{
try{
if(response != null){
response.close();
}
}catch(IOException e){
throw new Message(), e);
}
}
return null;
}
public static void main(String[] args){
String fileServer ="192.168.111.11/api/file/upload";
Map<String, String> headers =new HashMap<>();
Map<String, String> param =new HashMap<>();
param.put("title","⾮常⾼兴的⼀天");
param.put("moment","来⼀起看看这些漂亮的风景,⽔平有限,但⾃我感觉还是拍的不错的");
/*
重点注意这⾥,filesMap的构造要和后端的⽂件上传服务接⼝相配合
*/
Map<String, List<File>> filesMap =new HashMap<>();
File file1 =new File("/home/xxx/1.jpg");
File file2 =new File("/home/xxx/2.jpg");
filesMap.put("imgFiles", Arrays.asList(file1, file2));
File file3 =new File("/home/");
filesMap.put("txtFiles", Arrays.asList(file3));
/
*
以上的这种构造,后端的controller层,应该像如下这样接收:
@RequestMapping(value = "upload", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论