原文地址: http://www.itdecent.cn/p/2919bdb8d09a
- 解決Retrofit多BaseUrl及運(yùn)行時(shí)動(dòng)態(tài)改變BaseUrl(一)
- 解決Retrofit多BaseUrl及運(yùn)行時(shí)動(dòng)態(tài)改變BaseUrl(二)
前言
Hello,我是 JessYan,作為一個(gè)喜歡探索新穎解決方案的我,在 上篇文章 中,向大家介紹了怎樣通過一行代碼即可實(shí)現(xiàn)上傳下載以及 Glide 進(jìn)度監(jiān)聽,現(xiàn)在又給大家?guī)砹肆硪豁?xiàng)大家都很期待的問題的解決方案,這個(gè)問題起源于 MVPArms 的一個(gè) Issues ,當(dāng)然使用 Retrofit 時(shí),多個(gè) BaseUrl 以及動(dòng)態(tài)切換 BaseUrl 這兩個(gè)需求,在其他地方也經(jīng)常被討論,那么下面就來講講我的思路和解決方案
Github : 你的 Star 是我堅(jiān)持的動(dòng)力 ?

需求出現(xiàn)的場(chǎng)景
也許在日常開發(fā)中有些人已經(jīng)遇到了這兩個(gè)需求的場(chǎng)景,但為了讓一些之前沒遇到這些場(chǎng)景的朋友,也能看懂這篇文章,所以先在前面提一提
多個(gè) BaseUrl 的需求場(chǎng)景
如果項(xiàng)目是聚合型 App ,比如像一些新聞資訊類客戶端,可能數(shù)據(jù)源來自于多個(gè)平臺(tái),比如說知乎啊,豆瓣啊,今日頭條啊,所以這樣就會(huì)涉及到多個(gè) BaseUrl
如果項(xiàng)目使用到多個(gè)三方服務(wù)提供商,比如圖片的讀取使用到一個(gè)服務(wù)商,文件的存儲(chǔ)又使用到另一個(gè)服務(wù)商,這個(gè)也會(huì)存在一個(gè) App 出現(xiàn)多個(gè) BaseUrl
動(dòng)態(tài)改變 BaseUrl 的需求場(chǎng)景
如果項(xiàng)目的 BaseUrl 會(huì)在 App 啟動(dòng)時(shí),請(qǐng)求服務(wù)器,根據(jù)服務(wù)器的返回結(jié)果,來確定項(xiàng)目最終的 BaseUrl,就會(huì)涉及到運(yùn)行時(shí)動(dòng)態(tài)切換 BaseUrl
如果項(xiàng)目的某個(gè)三方服務(wù)提供商,并不是固定的,也許會(huì)出現(xiàn)變更的情況,比如存儲(chǔ)服務(wù)從七牛遷移至其他云存儲(chǔ),那我們?yōu)榱吮苊飧拇a導(dǎo)致重新打包以及發(fā)版,就會(huì)從服務(wù)器獲取三方服務(wù)提供商的 BaseUrl ,然后在運(yùn)行時(shí)動(dòng)態(tài)改變這個(gè) BaseUrl
解決方案
其實(shí)官方 Api 早已經(jīng)提供了解決方案來支持多個(gè) BaseUrl 以及運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl ,民間也同樣有很多解決方案
<a name="1"></a>
官方靜態(tài)解決方案
熟悉 Retrofit 的開發(fā)者應(yīng)該知道 @Get , @Post 這些標(biāo)注到每個(gè)接口方法上的注解不僅可以傳相對(duì)路徑,還可以傳全路徑,這樣我們就可以做到不同的接口使用不同的 BaseUrl ,從而達(dá)到使用多個(gè) BaseUrl 的需求,但是注解上的值只能是 Final 的常量,不能動(dòng)態(tài)改變,所以我稱這個(gè)解決方案為靜態(tài)解決方案
<a name="2"></a>
官方動(dòng)態(tài)解決方案
熟悉 Retrofit 的開發(fā)者也同樣知道 @Url 這個(gè)標(biāo)注到每個(gè)接口方法參數(shù)上的注解,它可以將全路徑作為參數(shù)傳進(jìn)接口作為每次請(qǐng)求的 Url 地址,每次請(qǐng)求接口都可以將不同的全路徑作為參數(shù),從而達(dá)到支持多個(gè) BaseUrl 以及在運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl ,所以很多請(qǐng)求圖片等資源的接口都是使用這個(gè)方案(咦,看樣子這個(gè)官方解決方案不是同時(shí)解決我提到的這兩個(gè)問題嗎,別急,先往后面看!)
<a name="3"></a>
民間常用解決方案
之前也看過很多開源的聚合類 App 源碼,像一些整合 知乎 , 豆瓣 , Gank 等多個(gè)平臺(tái)數(shù)據(jù)的 App ,因?yàn)楦髯云脚_(tái)的域名不同,所以大多數(shù)這類 App 會(huì)給每個(gè)平臺(tái)都各自創(chuàng)建一個(gè) Retrofit 對(duì)象,即不同的 BaseUrl 使用不同的 Retrofit 對(duì)象來創(chuàng)建 ApiService 進(jìn)行請(qǐng)求,這樣只要新增一個(gè)不同的 BaseUrl ,那就需要重新創(chuàng)建一個(gè)新的 Retrofit 對(duì)象
這樣也可以同時(shí)實(shí)現(xiàn),支持多個(gè) BaseUrl 以及運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl 這兩個(gè)需求,但是以個(gè)人的觀點(diǎn),創(chuàng)建多個(gè)其他配置屬性一模一樣,只是 BaseUrl 不一樣的 Retrofit 對(duì)象,太過于浪費(fèi)資源
<a name="4"></a>
民間大牛解決方案
之前偶然看到了一個(gè) Retrofit 維護(hù)者, Square 公司的大牛的 解決方案,用來解決運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl ,其實(shí)也算半官方的解決方案
提到這個(gè)解決方案時(shí),不得不講一個(gè)趣事,其實(shí)之前 Retrofit 默認(rèn)是支持運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl 的,以前是有一個(gè)名為 BaseUrl 的接口,而 Retrofit.Builder#baseUrl(BaseUrl) 方法當(dāng)時(shí)傳的參數(shù)就是這個(gè) BaseUrl ,而不是現(xiàn)在的 HttpUrl ,這個(gè)接口內(nèi)部就有一個(gè)方法返回 HttpUrl ,那時(shí)候只要實(shí)現(xiàn) BaseUrl 后,動(dòng)態(tài)改變這個(gè)方法的返回值,就可以實(shí)現(xiàn)動(dòng)態(tài)改變 BaseUrl
但是這位大牛認(rèn)為這樣的做法不安全,所以提了一個(gè) Pull Requests ,刪掉了這個(gè) BaseUrl 接口,并用上面的解決方案替代之,而親愛的 JakeWharton 同意了他的觀點(diǎn),并合并了這個(gè) PR 于是才有了現(xiàn)在的 Retrofit.Builder#baseUrl(HttpUrl) 這個(gè)不能動(dòng)態(tài)改變 BaseUrl 的 Api
用 Retrofit 比較早的老鳥,應(yīng)該知道以前有一個(gè)這個(gè) Api,我是說后來的版本怎么沒了,原來毀在了這位兄臺(tái)手上
這個(gè)方案也就是利用 Interceptor 攔截器,動(dòng)態(tài)改變每個(gè) Request 的 Url 從而實(shí)現(xiàn)動(dòng)態(tài)改變 BaseUrl,但他這個(gè)解決方案不能支持多 BaseUrl ,只要 host 一設(shè)置,直到下一次改變 Host 之前,后面的所有 Request 都必須使用同一個(gè) Host ,還有一些弊端后面一起分析
幾個(gè)方案的對(duì)比與分析
淘汰含有明顯缺陷的方案
4個(gè)方案中,我首先淘汰的就是 民間常用解決方案 ,在前面已經(jīng)明確了我的觀點(diǎn),因?yàn)槲覀€(gè)人認(rèn)為創(chuàng)建多個(gè)其他配置屬性一模一樣,只是 BaseUrl 不一樣的 Retrofit 對(duì)象,太過于浪費(fèi)資源,所以就算他能滿足我的所有需求,除非真的沒有更好的解決方案,否則我是不會(huì)選擇它的
剩下的三個(gè)方案中, 官方靜態(tài)解決方案 只能解決,2個(gè)需求中的支持多個(gè) BaseUrl ,而對(duì)于動(dòng)態(tài)改變 BaseUrl ,由于注解的 Value 只能為常量,所以對(duì)這個(gè)需求也是無能為力的(兩個(gè)需求都滿足,才表示可行)
誰是最優(yōu)方案?
其實(shí)在前面已經(jīng)說了 官方動(dòng)態(tài)解決方案 就已經(jīng)可以同時(shí)實(shí)現(xiàn)多 BaseUrl 和運(yùn)行時(shí)動(dòng)態(tài)改變 BaseUrl ,那為什么我不直接選擇這個(gè)方案,還要繼續(xù)分析呢?
答案也很簡(jiǎn)單,我認(rèn)為這個(gè)方案,雖然靈活,但是靈活卻給它帶來了使用上的繁瑣,每個(gè)接口每次調(diào)用都必須傳入全路徑作為參數(shù),不僅繁瑣而且接口一多還不好管理
那 民間大牛解決方案 可行? 但是我在前面已經(jīng)說了這個(gè)不可行啊?
這個(gè)方案雖然可以支持運(yùn)行時(shí)動(dòng)態(tài)切換 BaseUrl 但是它是全局處理,一經(jīng)使用改變的是所有請(qǐng)求的 Url ,所以它并不支持多 BaseUrl
并且更可怕的是,這個(gè)方案不僅不支持多 BaseUrl ,還會(huì)影響 官方靜態(tài)解決方案 和 官方動(dòng)態(tài)解決方案 這兩個(gè)支持多 BaseUrl 的方案,因?yàn)椴还苣阕⒔饫锩媛暶鞯氖鞘裁慈窂?它的 Interceptor 攔截器,都會(huì)強(qiáng)行將這個(gè)請(qǐng)求的 Url 改成它的 BaseUrl ,所以這個(gè)方案注定只適合只有一個(gè) BaseUrl 但需要?jiǎng)討B(tài)改變的項(xiàng)目
那豈不是 4 個(gè)解決方案都不可行?說這么久說個(gè)毛線啊?
方案全部淘汰?散會(huì)?
等等別急啊,雖然我站在我的角度, Pass 了文中提到的所有已存在的解決方案
但是大家仔細(xì)想想,如果網(wǎng)上已經(jīng)存在完美的解決方案,那我還寫這篇文章有什么意義?必定是沒有我滿意的解決方案,我才會(huì)自己動(dòng)手去解決并分享啊,畢竟我是一個(gè)不愿意寫重復(fù)內(nèi)容的有為青年,只要是我寫的內(nèi)容肯定是會(huì)讓大家學(xué)到不一樣的知識(shí)三 ?,不然不是砸自己招牌
好了,不逗大家了,開整!
別急,還有大招!
雖然在已有的解決方案當(dāng)中沒有找到讓我滿意的,但是在遇到問題時(shí),冷靜分析現(xiàn)有解決方案是很有必要的,理解前人的思路后才會(huì)對(duì)整個(gè)問題理解得更透徹,我的很多文章也都是以分析和解決思路為主,授人以魚不如授人以漁,所以我不會(huì)直接告訴你答案,先分析一波,理清思路
這不,在分析 民間大牛解決方案 時(shí),雖然最后發(fā)現(xiàn)這不是自己想要的解決方案,但是作為有發(fā)散思維的我,又是靈機(jī)一動(dòng),借助原有解決方案在上面這樣一改不是就可行了?
如何改善原有方案?
上面的分析已經(jīng)說了 民間大牛解決方案 ,可以在 Interceptor 攔截器中設(shè)置一個(gè)全局的 Host(Host 可以理解為 BaseUrl) ,攔截器會(huì)強(qiáng)行將這個(gè) Host 應(yīng)用到所有的請(qǐng)求上,改變?cè)撜?qǐng)求原有的 Url,這樣導(dǎo)致了只會(huì)同時(shí)存在一個(gè) Host
所以我在想,將這個(gè)唯一的 Host 變量改為集合,以存儲(chǔ)多個(gè) Host ,在將不同的 Host 應(yīng)用到不同的請(qǐng)求上,不就可以支持多 BaseUrl?
實(shí)踐想法
說干就干,于是我自己建了一個(gè)全局的容器來存儲(chǔ)多個(gè) Host,這樣我就可以在 App 運(yùn)行時(shí)的任何時(shí)間,任何地點(diǎn)隨意新增,修改,刪除 Host
遇到問題
但是問題來了,我想要將不同的 Host 應(yīng)用到不同的請(qǐng)求上,但我怎么知道什么請(qǐng)求需要什么樣的 Host ,每個(gè)請(qǐng)求總要有個(gè)標(biāo)記,讓我知道他需要什么樣的 Host 吧
于是我就在想 Retrofit 有什么方法,可以在請(qǐng)求之前給每個(gè)請(qǐng)求加上不同的字符串標(biāo)記,于是我很自然的想到了 Header ,Retrofit 正好有 @Headers 這個(gè)注解,可以給每個(gè)接口方法上加入自定義 Header
再次解決難點(diǎn)
我給需要不同 BaseUrl 的接口方法上加入了自定義的 Header ,以標(biāo)明每個(gè)接口需要的 Host 的 Name ,而這個(gè) Name 對(duì)應(yīng)的值就是 Host,但這個(gè)值不是在 @Headers 中被指定的,它是可以動(dòng)態(tài)改變的
存儲(chǔ) Host 的容器是一個(gè) Map, key 就是這個(gè) Name ,value 才是 Host ,攔截器每次攔截到請(qǐng)求時(shí),會(huì)判斷這個(gè)請(qǐng)求是否有這個(gè)自定義 Header,
有的話,拿到這個(gè) Header 中標(biāo)注的 Name,然后用這個(gè) Name ,去那個(gè)存儲(chǔ) Host 的全局 Map 中 get(name),拿到對(duì)應(yīng)的 Host 再應(yīng)用到請(qǐng)求上不是就達(dá)到支持多個(gè) BaseUrl 了?
如果想動(dòng)態(tài)改變某個(gè) Host 也簡(jiǎn)單,將新的 Host 以同樣的 Name put(name) 進(jìn)這個(gè)全局 Map ,到時(shí)候攔截器,使用這個(gè) Name get(name) 出來的值,就已經(jīng)是改變后最新的 Host ,在將這個(gè) Host 應(yīng)用到請(qǐng)求上不是就達(dá)到動(dòng)態(tài)改變 BaseUrl 了?
這不,兩個(gè)需求同時(shí)滿足!
優(yōu)化方案
這個(gè)方案就兩步,給需要不同 BaseUrl 的請(qǐng)求設(shè)置 Header (想用 Retrofit 默認(rèn) BaseUrl 的接口,或者使用 官方靜態(tài)解決方案, 官方動(dòng)態(tài)解決方案 就不需要設(shè)置),在通過全局容器來管理 BaseUrl
針對(duì)于那種只有一個(gè) BaseUrl 但需要?jiǎng)討B(tài)改變的項(xiàng)目,本框架提供了一個(gè) GlobalDomain 來優(yōu)化這個(gè)場(chǎng)景,不需要給接口加 Header ,只需要一步,向全局容器 put(GlobalDomain) 你想要改變的 BaseUrl 就可以了
比 官方動(dòng)態(tài)解決方案 給每個(gè)接口傳全路徑作為參數(shù),要簡(jiǎn)單的多, 官方動(dòng)態(tài)解決方案 注定只適合那種只有一兩個(gè)需要?jiǎng)討B(tài)改變 BaseUrl 的接口
總結(jié)
以上提到的解決方案,已經(jīng)優(yōu)化并封裝成了三方庫并上傳至 Jcenter,方便大家使用
本解決方案主要適合,需要同時(shí)具備多 BaseUrl 以及動(dòng)態(tài)改變 BaseUrl 的項(xiàng)目,或者只有一個(gè) BaseUrl ,但需要?jiǎng)討B(tài)改變 BaseUrl 的項(xiàng)目
如果對(duì)于只需要多 BaseUrl 不需要?jiǎng)討B(tài)改變 BaseUrl 的項(xiàng)目,其實(shí)用 官方靜態(tài)解決方案 就已經(jīng)足夠了,但我還是推薦用我的這個(gè)解決方案,因?yàn)樾枨蠖际菚?huì)變的,如果一旦要加入動(dòng)態(tài)改變 BaseUrl 的需求,如需要?jiǎng)討B(tài)切換 生產(chǎn)環(huán)境 和 開發(fā)環(huán)境 ,那這時(shí)怎么辦,一個(gè)個(gè)改掉每個(gè)接口注解里面的全路徑?
Github : 具體使用看 Demo ,記得 Star !
公眾號(hào)
掃碼關(guān)注我的公眾號(hào) JessYan,一起學(xué)習(xí)進(jìn)步,如果框架有更新,我也會(huì)在公眾號(hào)上第一時(shí)間通知大家
Hello 我叫Jessyan,如果您喜歡我的文章,可以在以下平臺(tái)關(guān)注我
- 個(gè)人主頁: http://jessyan.me
- GitHub: https://github.com/JessYanCoding
- 掘金: https://juejin.im/user/57a9dbd9165abd0061714613
- 簡(jiǎn)書: http://www.itdecent.cn/u/1d0c0bc634db
- 微博: http://weibo.com/u/1786262517
-- The end