Retrofit上传文件的参数设置疑问以及URL的坑

一些题外话...

很久没写过什么东西了,上一篇文章还要追溯到去年八月份。那时我还是单身,而现在,我他娘的还是单身。自从去年9月份以来,各种事情缠身算是彻底了我17年上半年无所事事的无聊遗憾。经过9月份的煎熬和无休止的争吵,终于在国庆回来后下定决心离职。随后就是漫长的工作交接和求职之路,出去正儿八经面试过了才知道锅儿是铁打的,其中的心酸可能也只有自己经历过了才能体会。好在在17年年末一切都终于尘埃落定,搞定了新工作。初来入职,任务不算紧,所以终于有时间和精力,更重要的是有心情来写一点东西。本来在去年计划来点货真价实的干货,写点视频录制的解决方案,不过也随着已成历史的17年无疾而终。自己也已经算是将近3个月没有正经经过代码的锤炼了,有时自己都感觉手指已经开始生疏。不过作为没什么人看的技术菜鸡,写什么年终总结、心路历程和鸡汤段子也显然不太合适。所以还是随便先写点东西练练手,不能就这么荒废下去。毕竟生活还要继续,用我去年领悟的一句话来说:假如生活欺骗了你,fuck it!

言归正传

最近刚好在准备公司打算另起炉灶的项目重构,重新搭建了基本框架,终于如愿以偿的用了一次RxJava+Retrofit+OKHttp的组合,所以就顺便说说关于自己遇到的关于Retrofit的一些的疑问。

Retrofit上传文件时设置配置参数的问题。

主要说说在文件上传时我们设置的一些参数。举个栗子,在很多上传的文章中,我们都能看到下面的代码:

@Multipart
@POST("mobile/upload")
Call<ResponseBody> upload(@Part MultipartBody.Part file);
RequestBody requestFile = RequestBody.create(MediaType.parse("image/jpg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);

第一段代码很好理解,定义了Retrofit的Service接口,我们实例化后根据先关用法就可以实现大致的上传逻辑了。第二段代码主要是传入我们需要上传的文件。那么问题来了,在很多文章出现的这两段代码,却很难找到其中的参数配置这么写的原因。@Multipart"image/jpg""file"这些参数究竟为什么要这么写?能不能改成其他的?很多时候我们只知道怎么做,但是不知道为什么,以至于一脚踩到坑里面也不知道怎么出来。

要想搞懂这几个配置的意义,还要从Http上传文件说起。最开始的Http是只能进行纯文本传输,所以在后来支持文件上传后,新增了一个MIME类型,叫multipart/form-data。
在纯文本上传时,请求头里会有这么一行

Content-Type:application/x-www-form-urlencoded; charset=UTF-8

而在上传文件时,请求头对应的类型属性需要定义成如下的方式:

Content-Type:multipart/form-data;  boundary=---------------------------238d787e8233c8874f

Content-Type:multipart/form-data;说明了这个请求的类型是用于文件上传,boundary的值是用于多个文件上传时的数据分隔,防止数据混在一起而无法解析,一般会用一段很难和正常文本重复的字符串。
上传时文件的具体信息是存放在Request Payload中的,下面是个简单的例子:

Request Payload
-----------------------------238d787e8233c8874f
Content-Disposition: form-data; name="yourKeyName"; filename="image1.jpg"
Content-Type: image/jpg
...
-----------------------------238d787e8233c8874f
Content-Disposition: form-data; name="upload2"; filename="image2.jpg"
Content-Type: image/jpg
...
-----------------------------238d787e8233c8874f--

上面的具体数据可能不是很严谨,但大概就是这么个意思。其中name规定了这个文件的名称,方便后台通过这个名称来获取,名字可以随便取,只要和后台约定统一即可;filename则是具体的上传文件的文件名;Content-Type规定了上传文件的后缀类型。

大致了解了Http上传的套路之后,回到最开始提到的那两端代码,这里为了方便不再往上翻,再次贴一下。

@Multipart
@POST("mobile/upload")
Call<ResponseBody> upload(@Part MultipartBody.Part file);
RequestBody requestFile = RequestBody.create(MediaType.parse("image/jpg"), file);
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
  • @Multipart
    我们在上文看到的Service接口中的@Multipart注解其实就是告诉Retrofit这个Service采用了multipart/form-data的请求方式,对应了请求头中Content-Type:multipart/form-data;这一部分。
  • MediaType.parse("image/jpg");
    "image/jpg"很好理解,是我们要上传的文件类型,你总得告诉别人你传的文件是什么格式的对吧,对应了请求体中的Content-Type: image/jpeg
  • MultipartBody.Part.createFormData("file", file.getName(), requestFile);
    至于"file"这个东西,就是一个单纯的keyName,方便服务端开发获取我们上传的文件,因为每一次请求可能会传多个文件,甚至是数据和文件同时上传,所以这个keyName就是服务端获取相应文件的依据。

到这里用retrofit上传文件的一些参数为什么要这样写大概就清楚了。不过上传文件的写法不止这一种,我们还经常看到下面这种写法:

 @Multipart    
 @POST("user/updateAvatar.do")
 Call<Response> upload(@Part("upload1\"; filename=\"image1.jpg\"") RequestBody imgs );

如果我们不知到Http上传文件的请求头和请求体的原理,这里我们看到@Part内的参数多半是懵逼的。不过与下面的代码对比一下,就能理解为什么这么写了。

Content-Disposition: form-data; name="upload1"; filename="image1.jpg"

@Part("upload1\"; filename=\"image1.jpg\"")中的参数经过转义后对应了upload1"; filename="image1.jpg" 这一段。这里要注意的是参数开头是没有引号的,而结尾有引号。

关于动态URL

扯完了文件上传,捣鼓了retrofit这么久,在动态url这一块也遇到了一些疑问,这里就顺便说说。
我们知道retrofit一个比较格式化的标准请求应该是下面这样:

 @GET("relativePath/...")//请求链接的相对路径
 Observable<ResponseResultModel<List<Express>>> getExpress(@Url String url, @Query("type") String type, @Query("postid") String postid);

然后在我们发起请求的时候,通过Retrofit.Builder().baseUrl("baseUrl")会给retrofit一个baseURL,两者拼接起来就是一个完整的路径。然而这种方式比较死板,所以在retrofit2.0开始引入了动态URL,我们无需在@GET注解或者其他注解上去写死相对路径,取而代之的是在Service接口方法中以参数的方式定义:

 @GET
 Observable<Response> downloadData(@Url String url);

当我们调用downloadData({relativeUrl})方法时,传入的相对URL就和baseURL拼接成了一个完整路径。
举个例子:

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://www.baidu.com/");
    .build();

MyService service = retrofit.create(MyService.class);  
service.login("user/login");

//拼接结果:https://www.baidu.com/user/login

这里我的URL是随便乱写的,不过原理大概是这个样子。这里需要注意一种特殊情况,我们对上面的代码进行一下修改:

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://www.baidu.com/v3");
    .build();

MyService service = retrofit.create(MyService.class);  
service.login("/user/login");

//拼接结果:https://www.baidu.com/user/login
//不是https://www.baidu.com/v3/user/login!!!
//不是https://www.baidu.com/v3/user/login!!!
//不是https://www.baidu.com/v3/user/login!!!

可以看到,最后的拼接结果并不是我们所想的那样。至于原因,是因为我们在节点URL前添加了一个"/"(service.login("/user/login")),这将导致retrofit在拼接链接时,忽略掉hostURL后面的内容,所以我们去掉节点URL前的"/"就可以了。

当然,我们也可以在参数中传入全路径,这时retrofit会自动解析这个完整路径,并不会再去和baseURL拼接:

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://www.baidu.com/");
    .build();

MyService service = retrofit.create(MyService.class);  
service.login("https://www.jianshu.com");

//结果:https://www.jianshu.com

后来突然想起来,如果在使用动态URL的同时,还给@GET注解添加节点URL会怎么样呢?

@GET
 Observable<Response> downloadData(@Url String url);

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://www.baidu.com/");
    .build();

MyService service = retrofit.create(MyService.class);  
service.login("user/login");

自己尝试跑了一下,直接就抛了异常,说明并没有这种操作。
最后,本文的目的并非系统而完全的介绍retrofit的体系和用法,只是记录和分享一些自己在使用过程中的疑问和自己了解到的答案。如果能有缘正好帮到有同样问题的人,那自然甚好。当然,如果对于文章有什么疑问或者建议,也欢迎大家留言交流。

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

推荐阅读更多精彩内容