轉(zhuǎn)載請(qǐng)注明出處:Retrofit2文件上傳
前言
使用Retrofit2已經(jīng)有一段時(shí)間了策菜,在使用時(shí)一直在感嘆庫的易用性和靈活性,一直想深入的研究下源碼和機(jī)制,但是項(xiàng)目催得緊贞岭,深陷泥潭無法脫身。果然在多文件上傳時(shí)被卡住了饱狂。(今天犯懶曹步,明天就遭報(bào)應(yīng))研究半天終于跑通,特此記錄休讳。
Http MultiPart消息
其實(shí)無論什么庫讲婚,只要是發(fā)送Http請(qǐng)求,都得遵守Http協(xié)議俊柔,所以熟悉協(xié)議內(nèi)容對(duì)理解庫原理筹麸、調(diào)試是有很大幫助的。
Http上傳協(xié)議為MultiPart雏婶。下面是通過抓包獲取的一次多文件+文本的上傳消息物赶,每行前面的行數(shù)是為了標(biāo)注說明方便加上的,實(shí)際請(qǐng)求中沒有留晚。
1 POST http://host:8080/updata.action HTTP/1.1
2 Content-Type: multipart/form-data; boundary=bec890b3-d76c-4986-803d-dc4b57ba2421
3 Content-Length: 3046505
4 Host: host:8080
5 Connection: Keep-Alive
6 Accept-Encoding: gzip
7 User-Agent: okhttp/3.2.0
8
9 --bec890b3-d76c-4986-803d-dc4b57ba2421
10 Content-Disposition: form-data; name="title"
11 Content-Type: text/plain; charset=utf-8
12 Content-Length: 15
13
14 多文件上傳
15 --bec890b3-d76c-4986-803d-dc4b57ba2421
16 Content-Disposition: form-data; name="token"
17 Content-Type: text/plain; charset=utf-8
18 Content-Length: 32
19
20 登陸Token值
21 --776becce-5bd0-41d3-aa73-d3cd3ca4209d
22 Content-Disposition: form-data; name="imgUrls"; filename="0.jpg"
23 Content-Type: image/*
24 Content-Length: 168637
25
26 (文件字節(jié)酵紫,一堆亂碼)@ h r q UY? e<?* ? 7C Z 6?...
27 --776becce-5bd0-41d3-aa73-d3cd3ca4209d
28 Content-Disposition: form-data; name="imgUrls"; filename="1.jpg"
29 Content-Type: image/*
30 Content-Length: 164004
31
32 (文件字節(jié),一堆亂碼)@ h r q UY? e<?* ? 7C Z 6?...
33 --776becce-5bd0-41d3-aa73-d3cd3ca4209d
34 Content-Disposition: form-data; name="imgUrls"; filename="2.jpg"
35 Content-Type: image/*
36 Content-Length: 167307
37
38 (文件字節(jié),一堆亂碼)@ h r q UY? e<?* ? 7C Z 6?...
39 --776becce-5bd0-41d3-aa73-d3cd3ca4209d--
- line1:請(qǐng)求行
- line2-line7:消息頭
- line2:定義請(qǐng)求類型及分隔符
- line9-line39:消息正文
- line9:分隔符奖地,用于分割正文的各條數(shù)據(jù)
- line39:結(jié)尾分隔符
- line10:name定義服務(wù)端獲取本條數(shù)據(jù)的key
- line17:Content-Type定義本條數(shù)據(jù)類型為文本橄唬,charset定義編碼為utf-8
- line22:name定義Key,filename定義上傳的文件名
- line23:Content-Type定義本條數(shù)據(jù)類型為圖片文件
以上代碼為一次多文件+文本的表單請(qǐng)求参歹,Retrofit2基本將能封裝的內(nèi)容都封裝了仰楚,我們需要做的就是通過MultiPartBody.Part或者M(jìn)ultiPartBody將文本及文件數(shù)據(jù)封裝好并傳到接口中。
Retrofit2實(shí)現(xiàn)上傳請(qǐng)求
上面說到Retrofit2封裝請(qǐng)求消息是不完全正確的犬庇,因?yàn)镽etrofit2使用動(dòng)態(tài)代理將具體的請(qǐng)求分發(fā)給具體的http client去執(zhí)行僧界,一般使用Okhttp。
定義上傳接口
/**
* 注意1:必須使用{@code @POST}注解為post請(qǐng)求<br>
* 注意:使用{@code @Multipart}注解方法臭挽,必須使用{@code @Part}/<br>
* {@code @PartMap}注解其參數(shù)<br>
* 本接口中將文本數(shù)據(jù)和文件數(shù)據(jù)分為了兩個(gè)參數(shù)捂襟,是為了方便將封裝<br>
* {@link MultipartBody.Part}的代碼抽取到工具類中<br>
* 也可以合并成一個(gè){@code @Part}參數(shù)
* @param params 用于封裝文本數(shù)據(jù)
* @param parts 用于封裝文件數(shù)據(jù)
* @return BaseResp為服務(wù)器返回的基本Json數(shù)據(jù)的Model類
*/
@Multipart
@POST(RequestApiPath.UPLOAD_WORK)
Observable<BaseResp> requestUploadWork(@PartMap Map<String, RequestBody> params,
@Part List<MultipartBody.Part> parts);
/**
* 注意1:必須使用{@code @POST}注解為post請(qǐng)求<br>
* 注意2:使用{@code @Body}注解參數(shù),則不能使用{@code @Multipart}注解方法了<br>
* 直接將所有的{@link MultipartBody.Part}合并到一個(gè){@link MultipartBody}中
*/
@POST(RequestApiPath.UPLOAD_WORK)
Observable<BaseResp> requestUploadWork(@Body MultipartBody body);
MultipartBody.Part/MultipartBody的封裝
/**
* 將文件路徑數(shù)組封裝為{@link List<MultipartBody.Part>}
* @param key 對(duì)應(yīng)請(qǐng)求正文中name的值埋哟。目前服務(wù)器給出的接口中笆豁,所有圖片文件使用<br>
* 同一個(gè)name值,實(shí)際情況中有可能需要多個(gè)
* @param filePaths 文件路徑數(shù)組
* @param imageType 文件類型
*/
public static List<MultipartBody.Part> files2Parts(String key,
String[] filePaths, MediaType imageType) {
List<MultipartBody.Part> parts = new ArrayList<>(filePaths.length);
for (String filePath : filePaths) {
File file = new File(filePath);
// 根據(jù)類型及File對(duì)象創(chuàng)建RequestBody(okhttp的類)
RequestBody requestBody = RequestBody.create(imageType, file);
// 將RequestBody封裝成MultipartBody.Part類型(同樣是okhttp的)
MultipartBody.Part part = MultipartBody.Part.
createFormData(key, file.getName(), requestBody);
// 添加進(jìn)集合
parts.add(part);
}
return parts;
}
/**
* 其實(shí)也是將File封裝成RequestBody赤赊,然后再封裝成Part闯狱,<br>
* 不同的是使用MultipartBody.Builder來構(gòu)建MultipartBody
* @param key 同上
* @param filePaths 同上
* @param imageType 同上
*/
public static MultipartBody filesToMultipartBody(String key,
String[] filePaths,
MediaType imageType) {
MultipartBody.Builder builder = new MultipartBody.Builder();
for (String filePath : filePaths) {
File file = new File(filePath);
RequestBody requestBody = RequestBody.create(imageType, file);
builder.addFormDataPart(key, file.getName(), requestBody);
}
builder.setType(MultipartBody.FORM);
return builder.build();
}
文本類型的MultipartBody.Part封裝
/**
* 直接添加文本類型的Part到的MultipartBody的Part集合中
* @param parts Part集合
* @param key 參數(shù)名(name屬性)
* @param value 文本內(nèi)容
* @param position 插入的位置
*/
public static void addTextPart(List<MultipartBody.Part> parts,
String key, String value, int position) {
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), value);
MultipartBody.Part part = MultipartBody.Part.createFormData(key, null, requestBody);
parts.add(position, part);
}
/**
* 添加文本類型的Part到的MultipartBody.Builder中
* @param builder 用于構(gòu)建MultipartBody的Builder
* @param key 參數(shù)名(name屬性)
* @param value 文本內(nèi)容
*/
public static MultipartBody.Builder addTextPart(MultipartBody.Builder builder,
String key, String value) {
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), value);
// MultipartBody.Builder的addFormDataPart()有一個(gè)直接添加key value的重載,但坑的是這個(gè)方法
// 不會(huì)設(shè)置編碼類型抛计,會(huì)出亂碼哄孤,所以可以使用3個(gè)參數(shù)的,將中間的filename置為null就可以了
// builder.addFormDataPart(key, value);
// 還有一個(gè)坑就是吹截,后臺(tái)取數(shù)據(jù)的時(shí)候有可能是有順序的瘦陈,比如必須先取文本后取文件,
// 否則就取不到(真弱啊...)波俄,所以還要注意add的順序
builder.addFormDataPart(key, null, requestBody);
return builder;
}