Retrofit2文件上传

转载请注明出处: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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,914评论 25 707
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful2、Retrofit解析2...
    隔壁老李头阅读 15,068评论 4 39
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,644评论 18 139
  • 自从联合国做了个声明,“「青年」的定义是年龄介于15岁与24岁之间的群体。” 万千90后(尤其90-92年为甚)的...
    怪味wiwi阅读 417评论 0 5
  • 一个朋友给我发来一篇微信文章《阿里不去清华招人的真正原因》。并附上一句话:“没有效率的增长,不是慢性自杀,而是加速...
    燕灵湾阅读 272评论 0 0