轉載請注明出處:Retrofit2文件上傳
前言
使用Retrofit2已經有一段時間了,在使用時一直在感嘆庫的易用性和靈活性,一直想深入的研究下源碼和機制,但是項目催得緊,深陷泥潭無法脫身。果然在多文件上傳時被卡住了。(今天犯懶,明天就遭報應)研究半天終于跑通,特此記錄。
Http MultiPart消息
其實無論什么庫,只要是發送Http請求,都得遵守Http協議,所以熟悉協議內容對理解庫原理、調試是有很大幫助的。
Http上傳協議為MultiPart。下面是通過抓包獲取的一次多文件+文本的上傳消息,每行前面的行數是為了標注說明方便加上的,實際請求中沒有。
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 (文件字節,一堆亂碼)@ 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 (文件字節,一堆亂碼)@ 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 (文件字節,一堆亂碼)@ h r q UY? e<?* ? 7C Z 6?...
39 --776becce-5bd0-41d3-aa73-d3cd3ca4209d--
- line1:請求行
- line2-line7:消息頭
- line2:定義請求類型及分隔符
- line9-line39:消息正文
- line9:分隔符,用于分割正文的各條數據
- line39:結尾分隔符
- line10:name定義服務端獲取本條數據的key
- line17:Content-Type定義本條數據類型為文本,charset定義編碼為utf-8
- line22:name定義Key,filename定義上傳的文件名
- line23:Content-Type定義本條數據類型為圖片文件
以上代碼為一次多文件+文本的表單請求,Retrofit2基本將能封裝的內容都封裝了,我們需要做的就是通過MultiPartBody.Part或者MultiPartBody將文本及文件數據封裝好并傳到接口中。
Retrofit2實現上傳請求
上面說到Retrofit2封裝請求消息是不完全正確的,因為Retrofit2使用動態代理將具體的請求分發給具體的http client去執行,一般使用Okhttp。
定義上傳接口
/**
* 注意1:必須使用{@code @POST}注解為post請求<br>
* 注意:使用{@code @Multipart}注解方法,必須使用{@code @Part}/<br>
* {@code @PartMap}注解其參數<br>
* 本接口中將文本數據和文件數據分為了兩個參數,是為了方便將封裝<br>
* {@link MultipartBody.Part}的代碼抽取到工具類中<br>
* 也可以合并成一個{@code @Part}參數
* @param params 用于封裝文本數據
* @param parts 用于封裝文件數據
* @return BaseResp為服務器返回的基本Json數據的Model類
*/
@Multipart
@POST(RequestApiPath.UPLOAD_WORK)
Observable<BaseResp> requestUploadWork(@PartMap Map<String, RequestBody> params,
@Part List<MultipartBody.Part> parts);
/**
* 注意1:必須使用{@code @POST}注解為post請求<br>
* 注意2:使用{@code @Body}注解參數,則不能使用{@code @Multipart}注解方法了<br>
* 直接將所有的{@link MultipartBody.Part}合并到一個{@link MultipartBody}中
*/
@POST(RequestApiPath.UPLOAD_WORK)
Observable<BaseResp> requestUploadWork(@Body MultipartBody body);
MultipartBody.Part/MultipartBody的封裝
/**
* 將文件路徑數組封裝為{@link List<MultipartBody.Part>}
* @param key 對應請求正文中name的值。目前服務器給出的接口中,所有圖片文件使用<br>
* 同一個name值,實際情況中有可能需要多個
* @param filePaths 文件路徑數組
* @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);
// 根據類型及File對象創建RequestBody(okhttp的類)
RequestBody requestBody = RequestBody.create(imageType, file);
// 將RequestBody封裝成MultipartBody.Part類型(同樣是okhttp的)
MultipartBody.Part part = MultipartBody.Part.
createFormData(key, file.getName(), requestBody);
// 添加進集合
parts.add(part);
}
return parts;
}
/**
* 其實也是將File封裝成RequestBody,然后再封裝成Part,<br>
* 不同的是使用MultipartBody.Builder來構建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 參數名(name屬性)
* @param value 文本內容
* @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 用于構建MultipartBody的Builder
* @param key 參數名(name屬性)
* @param value 文本內容
*/
public static MultipartBody.Builder addTextPart(MultipartBody.Builder builder,
String key, String value) {
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), value);
// MultipartBody.Builder的addFormDataPart()有一個直接添加key value的重載,但坑的是這個方法
// 不會設置編碼類型,會出亂碼,所以可以使用3個參數的,將中間的filename置為null就可以了
// builder.addFormDataPart(key, value);
// 還有一個坑就是,后臺取數據的時候有可能是有順序的,比如必須先取文本后取文件,
// 否則就取不到(真弱啊...),所以還要注意add的順序
builder.addFormDataPart(key, null, requestBody);
return builder;
}