一些题外话...
很久没写过什么东西了,上一篇文章还要追溯到去年八月份。那时我还是单身,而现在,我他娘的还是单身。自从去年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的体系和用法,只是记录和分享一些自己在使用过程中的疑问和自己了解到的答案。如果能有缘正好帮到有同样问题的人,那自然甚好。当然,如果对于文章有什么疑问或者建议,也欢迎大家留言交流。