一些題外話...
很久沒(méi)寫過(guò)什么東西了,上一篇文章還要追溯到去年八月份。那時(shí)我還是單身,而現(xiàn)在,我他娘的還是單身。自從去年9月份以來(lái),各種事情纏身算是徹底了我17年上半年無(wú)所事事的無(wú)聊遺憾。經(jīng)過(guò)9月份的煎熬和無(wú)休止的爭(zhēng)吵,終于在國(guó)慶回來(lái)后下定決心離職。隨后就是漫長(zhǎng)的工作交接和求職之路,出去正兒八經(jīng)面試過(guò)了才知道鍋兒是鐵打的,其中的心酸可能也只有自己經(jīng)歷過(guò)了才能體會(huì)。好在在17年年末一切都終于塵埃落定,搞定了新工作。初來(lái)入職,任務(wù)不算緊,所以終于有時(shí)間和精力,更重要的是有心情來(lái)寫一點(diǎn)東西。本來(lái)在去年計(jì)劃來(lái)點(diǎn)貨真價(jià)實(shí)的干貨,寫點(diǎn)視頻錄制的解決方案,不過(guò)也隨著已成歷史的17年無(wú)疾而終。自己也已經(jīng)算是將近3個(gè)月沒(méi)有正經(jīng)經(jīng)過(guò)代碼的錘煉了,有時(shí)自己都感覺(jué)手指已經(jīng)開(kāi)始生疏。不過(guò)作為沒(méi)什么人看的技術(shù)菜雞,寫什么年終總結(jié)、心路歷程和雞湯段子也顯然不太合適。所以還是隨便先寫點(diǎn)東西練練手,不能就這么荒廢下去。畢竟生活還要繼續(xù),用我去年領(lǐng)悟的一句話來(lái)說(shuō):假如生活欺騙了你,fuck it!
言歸正傳
最近剛好在準(zhǔn)備公司打算另起爐灶的項(xiàng)目重構(gòu),重新搭建了基本框架,終于如愿以償?shù)挠昧艘淮蜶xJava+Retrofit+OKHttp的組合,所以就順便說(shuō)說(shuō)關(guān)于自己遇到的關(guān)于Retrofit的一些的疑問(wèn)。
Retrofit上傳文件時(shí)設(shè)置配置參數(shù)的問(wèn)題。
主要說(shuō)說(shuō)在文件上傳時(shí)我們?cè)O(shè)置的一些參數(shù)。舉個(gè)栗子,在很多上傳的文章中,我們都能看到下面的代碼:
@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接口,我們實(shí)例化后根據(jù)先關(guān)用法就可以實(shí)現(xiàn)大致的上傳邏輯了。第二段代碼主要是傳入我們需要上傳的文件。那么問(wèn)題來(lái)了,在很多文章出現(xiàn)的這兩段代碼,卻很難找到其中的參數(shù)配置這么寫的原因。@Multipart
、"image/jpg"
、"file"
這些參數(shù)究竟為什么要這么寫?能不能改成其他的?很多時(shí)候我們只知道怎么做,但是不知道為什么,以至于一腳踩到坑里面也不知道怎么出來(lái)。
要想搞懂這幾個(gè)配置的意義,還要從Http上傳文件說(shuō)起。最開(kāi)始的Http是只能進(jìn)行純文本傳輸,所以在后來(lái)支持文件上傳后,新增了一個(gè)MIME類型,叫multipart/form-data。
在純文本上傳時(shí),請(qǐng)求頭里會(huì)有這么一行
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
而在上傳文件時(shí),請(qǐng)求頭對(duì)應(yīng)的類型屬性需要定義成如下的方式:
Content-Type:multipart/form-data; boundary=---------------------------238d787e8233c8874f
Content-Type:multipart/form-data;
說(shuō)明了這個(gè)請(qǐng)求的類型是用于文件上傳,boundary
的值是用于多個(gè)文件上傳時(shí)的數(shù)據(jù)分隔,防止數(shù)據(jù)混在一起而無(wú)法解析,一般會(huì)用一段很難和正常文本重復(fù)的字符串。
上傳時(shí)文件的具體信息是存放在Request Payload中的,下面是個(gè)簡(jiǎn)單的例子:
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--
上面的具體數(shù)據(jù)可能不是很嚴(yán)謹(jǐn),但大概就是這么個(gè)意思。其中name
規(guī)定了這個(gè)文件的名稱,方便后臺(tái)通過(guò)這個(gè)名稱來(lái)獲取,名字可以隨便取,只要和后臺(tái)約定統(tǒng)一即可;filename
則是具體的上傳文件的文件名;Content-Type
規(guī)定了上傳文件的后綴類型。
大致了解了Http上傳的套路之后,回到最開(kāi)始提到的那兩端代碼,這里為了方便不再往上翻,再次貼一下。
@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
我們?cè)谏衔目吹降腟ervice接口中的@Multipart
注解其實(shí)就是告訴Retrofit這個(gè)Service采用了multipart/form-data的請(qǐng)求方式,對(duì)應(yīng)了請(qǐng)求頭中Content-Type:multipart/form-data;
這一部分。 - MediaType.parse("image/jpg");
"image/jpg"很好理解,是我們要上傳的文件類型,你總得告訴別人你傳的文件是什么格式的對(duì)吧,對(duì)應(yīng)了請(qǐng)求體中的Content-Type: image/jpeg
。 - MultipartBody.Part.createFormData("file", file.getName(), requestFile);
至于"file"這個(gè)東西,就是一個(gè)單純的keyName,方便服務(wù)端開(kāi)發(fā)獲取我們上傳的文件,因?yàn)槊恳淮握?qǐng)求可能會(huì)傳多個(gè)文件,甚至是數(shù)據(jù)和文件同時(shí)上傳,所以這個(gè)keyName就是服務(wù)端獲取相應(yīng)文件的依據(jù)。
到這里用retrofit上傳文件的一些參數(shù)為什么要這樣寫大概就清楚了。不過(guò)上傳文件的寫法不止這一種,我們還經(jīng)常看到下面這種寫法:
@Multipart
@POST("user/updateAvatar.do")
Call<Response> upload(@Part("upload1\"; filename=\"image1.jpg\"") RequestBody imgs );
如果我們不知到Http上傳文件的請(qǐng)求頭和請(qǐng)求體的原理,這里我們看到@Part內(nèi)的參數(shù)多半是懵逼的。不過(guò)與下面的代碼對(duì)比一下,就能理解為什么這么寫了。
Content-Disposition: form-data; name="upload1"; filename="image1.jpg"
@Part("upload1\"; filename=\"image1.jpg\"")
中的參數(shù)經(jīng)過(guò)轉(zhuǎn)義后對(duì)應(yīng)了upload1"; filename="image1.jpg" 這一段。這里要注意的是參數(shù)開(kāi)頭是沒(méi)有引號(hào)的,而結(jié)尾有引號(hào)。
關(guān)于動(dòng)態(tài)URL
扯完了文件上傳,搗鼓了retrofit這么久,在動(dòng)態(tài)url這一塊也遇到了一些疑問(wèn),這里就順便說(shuō)說(shuō)。
我們知道retrofit一個(gè)比較格式化的標(biāo)準(zhǔn)請(qǐng)求應(yīng)該是下面這樣:
@GET("relativePath/...")//請(qǐng)求鏈接的相對(duì)路徑
Observable<ResponseResultModel<List<Express>>> getExpress(@Url String url, @Query("type") String type, @Query("postid") String postid);
然后在我們發(fā)起請(qǐng)求的時(shí)候,通過(guò)Retrofit.Builder().baseUrl("baseUrl")
會(huì)給retrofit一個(gè)baseURL,兩者拼接起來(lái)就是一個(gè)完整的路徑。然而這種方式比較死板,所以在retrofit2.0開(kāi)始引入了動(dòng)態(tài)URL,我們無(wú)需在@GET注解或者其他注解上去寫死相對(duì)路徑,取而代之的是在Service接口方法中以參數(shù)的方式定義:
@GET
Observable<Response> downloadData(@Url String url);
當(dāng)我們調(diào)用downloadData({relativeUrl})方法時(shí),傳入的相對(duì)URL就和baseURL拼接成了一個(gè)完整路徑。
舉個(gè)例子:
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://www.baidu.com/");
.build();
MyService service = retrofit.create(MyService.class);
service.login("user/login");
//拼接結(jié)果:https://www.baidu.com/user/login
這里我的URL是隨便亂寫的,不過(guò)原理大概是這個(gè)樣子。這里需要注意一種特殊情況,我們對(duì)上面的代碼進(jìn)行一下修改:
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://www.baidu.com/v3");
.build();
MyService service = retrofit.create(MyService.class);
service.login("/user/login");
//拼接結(jié)果: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!!!
可以看到,最后的拼接結(jié)果并不是我們所想的那樣。至于原因,是因?yàn)槲覀冊(cè)诠?jié)點(diǎn)URL前添加了一個(gè)"/"(service.login("/user/login")
),這將導(dǎo)致retrofit在拼接鏈接時(shí),忽略掉hostURL后面的內(nèi)容,所以我們?nèi)サ艄?jié)點(diǎn)URL前的"/"就可以了。
當(dāng)然,我們也可以在參數(shù)中傳入全路徑,這時(shí)retrofit會(huì)自動(dòng)解析這個(gè)完整路徑,并不會(huì)再去和baseURL拼接:
Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://www.baidu.com/");
.build();
MyService service = retrofit.create(MyService.class);
service.login("http://www.lxweimin.com");
//結(jié)果:http://www.lxweimin.com
后來(lái)突然想起來(lái),如果在使用動(dòng)態(tài)URL的同時(shí),還給@GET注解添加節(jié)點(diǎn)URL會(huì)怎么樣呢?
@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");
自己嘗試跑了一下,直接就拋了異常,說(shuō)明并沒(méi)有這種操作。
最后,本文的目的并非系統(tǒng)而完全的介紹retrofit的體系和用法,只是記錄和分享一些自己在使用過(guò)程中的疑問(wèn)和自己了解到的答案。如果能有緣正好幫到有同樣問(wèn)題的人,那自然甚好。當(dāng)然,如果對(duì)于文章有什么疑問(wèn)或者建議,也歡迎大家留言交流。