Retrofit2文件上传

记录Retrofit2上传文件一些关键点

Posted by Channing on July 11, 2016

前言

使用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请求。
 * 注意2:使用{@code @Multipart}注解方法的时候,只能使用{@code @Part}或者{@code @PartMap}
 * 注解其参数。
 * 本接口中将文本数据和文件数据分为了两个参数,是为了方便将构建{@link MultipartBody.Part}
 * 的代码抽取到工具类中,你也可以合并成一个{@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请求。
 * 注意2:使用{@code @Body}注解参数,则不能使用{@code @Multipart}注解方法了
 * 直接将所有的{@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的值。目前服务器给出的接口中,所有图片文件使用
 * 同一个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,
 * 不同的是使用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;
}

转载请注明出处:Retrofit2文件上传