使用Retrofit+Okhttp進行請求的項目應該挺多的,頗有可能會遇到一個需求。
就是能夠動態的修改Retrofit+Okhttp框架下的請求地址(BaseUrl),這樣就但是實現各類後臺環境下的請求切換。
而Retrofit又沒有提供一個較爲方便好用的切換BaseUrl的方法,那麼就要尋找別的途徑來解決這個問題。java
Retrofit攔截器的主要做用在於對網絡傳輸的數據進行攔截和處理。經過攔截器攔截即將發出的請求及對響應結果作相應處理,典型的處理方式是修改header添加一下特定的參數,如後臺須要的token、deviceId、渠道號等參數。既然攔截器能夠進行這些參數的修改,就也能夠對請求的url進行處理。攔截器有兩種:git
處理header等參數能夠在Interceptor中處理,建立Interceptor的對象,其提供了一個方法intercept(Chain chain)
。
其中chain對象就能夠拿到請求的request,而後進行一些處理。github
Interceptor headInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("Content-Type", "application/json; charset=UTF-8")
.addHeader("token", XXXXXX.getToken())
.build();
return chain.proceed(request);
}
};
//而後經過addInterceptor將迭代器設置給OkhttClient
builder.addInterceptor(headInterceptor);
複製代碼
以上就是經過Interceptor對Header進行的一些操做,那麼經過攔截器也能夠處理請求的BaseUrl。json
Interceptor BaseUrlInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
// 獲取request
Request request = chain.request();
// 獲取request的建立者builder
Request.Builder builder = request.newBuilder();
// 從request中獲取headers,經過給定的鍵url_name
List<String> headerValues = request.headers("url_name");
if (headerValues != null && headerValues.size() > 0) {
// 若是有這個header,先將配置的header刪除,所以header僅用做app和okhttp之間使用
builder.removeHeader("url_name");
// 匹配得到新的BaseUrl
String headerValue = headerValues.get(0);
HttpUrl newBaseUrl = null;
if ("test".equals(headerValue)) {
newBaseUrl = HttpUrl.parse("測試地址");
} else if ("online".equals(headerValue)) {
newBaseUrl = HttpUrl.parse("正式路徑");
} else {
newBaseUrl = request.url();
}
// 重建新的HttpUrl,修改須要修改的url部分
HttpUrl newFullUrl = newBaseUrl
.newBuilder()
// 更換網絡協議
.scheme(newBaseUrl.scheme())
// 更換主機名
.host(newBaseUrl.host())
// 更換端口
.port(newBaseUrl.port())
.build();
// 重建這個request,經過builder.url(newFullUrl).build();
// 而後返回一個response至此結束脩改
return chain.proceed(builder.url(newFullUrl).build());
}
}
};
//而後設置此攔截器給OkhttpClient
builder.addInterceptor(BaseUrlInterceptor);
//經過Retrofit構建請求的時候須要添加Header參數
@Headers("可切換的BaseUrl")
@FormUrlEncoded
@POST(LOGIN_LOGIN)
Observable<ObjectResponse> mLoginAPI(@FieldMap Map<String, Object> params);
複製代碼
以上方式能夠在某個接口修改請求的url,可是不可以動態的去更換請求的url。緩存
這個攔截器主要處理請求數據的展現,方便於調試用,須要導入攔截器的擴展包。
com.squareup.okhttp3:logging-interceptor:3.8.1
微信
要想經過反射來修改請求的BaseUrl,首先須要瞭解修改的字段是那些,在什麼地方。因此須要對Retrofit的源碼進行查看:
Retrofit是經過Build去構建請求參數的:網絡
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("請求的url")
... ...
複製代碼
因此.baseUrl()
方式就是切入點,查看其代碼的實現:app
public Retrofit.Builder baseUrl(String baseUrl) {
Utils.checkNotNull(baseUrl, "baseUrl == null");
//在此將設置的baseUrl設置給了HttpUrl
HttpUrl httpUrl = HttpUrl.parse(baseUrl);
if (httpUrl == null) {
throw new IllegalArgumentException("Illegal URL: " + baseUrl);
} else {
return this.baseUrl(httpUrl);
}
}
複製代碼
好了,經過這個Retroift提供的baseUrl()方法能夠清楚的看到,其將baseUrl設置給了HttpUrl。
那麼在Retrofit中確定有HttpUrl的對象:框架
public final class Retrofit {
//請記住這個參數,下面要用到
private final Map<Method, ServiceMethod<?, ?>> serviceMethodCache = new ConcurrentHashMap<>();
final okhttp3.Call.Factory callFactory;
//HttpUrl的對象
final HttpUrl baseUrl;
final List<Converter.Factory> converterFactories;
final List<CallAdapter.Factory> callAdapterFactories;
final @Nullable Executor callbackExecutor;
final boolean validateEagerly;
... ...
}
複製代碼
那麼這個HttpUrl又是什麼對象呢?查看其源碼:ide
package okhttp3;
import okhttp3.internal.Util;
import ... ...;
public final class HttpUrl {
... ...
static final String USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
static final String PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#";
static final String PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#";
static final String PATH_SEGMENT_ENCODE_SET_URI = "[]";
static final String QUERY_ENCODE_SET = " \"'<>#";
static final String QUERY_COMPONENT_ENCODE_SET = " \"'<>#&=";
static final String QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}";
static final String FORM_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#&!$(),~";
static final String FRAGMENT_ENCODE_SET = "";
static final String FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}";
final String scheme;
private final String username;
private final String password;
final String host;
final int port;
private final List<String> pathSegments;
@Nullable
private final List<String> queryNamesAndValues;
@Nullable
private final String fragment;
private final String url;
... ...
}
複製代碼
看到這裏,能夠很清楚的看到,這個HttpUrl居然是okhttp3包下的類。
那麼Retrofit+OkHttp中說到:
Retrofit負責請求的裝配,OkHttp負責底層的請求,就很好解釋了。
順着這條思路,繼續往下挖掘,既然Okhttp負責請求,那麼應該在其中能夠找到跟路徑有關的地方:
//請求主機
final String host;
//請求端口
final int port;
//請求url
private final String url;
複製代碼
看到這三個字段,咱們徹底找到了反射所須要的切入點,只須要經過反射修改這三個字段便可。
首先咱們須要獲取HttpUrl的對象:
HttpUrl httpUrl = RetrofitSingleton.retrofit.baseUrl();
複製代碼
而後進行反射操做:
public static class Http {
public Http(String url, String host, int port) {
this.url = url;
this.host = host;
this.port = port;
}
public String url; //對應HttpUrl的url
public String host; //對應HttpUrl的host
public int port; //對應HttpUrl的port
}
public static boolean hookRetrofitUrl(AboutUsActivity.Http http) {
if (http == null) {
return false;
}
try {
//獲取HttpUrl對象
Class<?> httpClass = Class.forName("okhttp3.HttpUrl");
HttpUrl httpUrl = HttpModule.RETROFIT.baseUrl();
//修改url
Field url = httpClass.getDeclaredField("url");
url.setAccessible(true);
url.set(httpUrl, http.url);
//修改host
Field host = httpClass.getDeclaredField("host");
host.setAccessible(true);
host.set(httpUrl, http.host);
//修改port端口號
Field port = httpClass.getDeclaredField("port");
port.setAccessible(true);
port.set(httpUrl, http.port);
//獲取Retrofit
Class<Retrofit> retrofitClass = Retrofit.class;
Field baseUrlField = retrofitClass.getDeclaredField("baseUrl");
//修改baseUrl(baseUrl爲Retrofit中的HttpUrl對象,其實就是將對象替換掉)
baseUrlField.setAccessible(true);
baseUrlField.set(HttpModule.RETROFIT, httpUrl);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
複製代碼
這裏咱們一共作了6步操做:
到此就完成了對Retfofit BaseUrl的修改,可是通過測試發現請求路徑仍是原路徑。這是爲何呢?既然沒有修改爲功,那確定是某些地方發生了一些不可描述的問題。
再次從Retrofit進行梳理,請你們瀏覽一下 一、反射的切入點 第三個代碼片斷,能夠看到這樣Retforit持有這樣一個對象:
//原來這個對象是Retrofit對請求的方法的Cache緩存。
private final Map<Method, ServiceMethod<?, ?>> serviceMethodCache = new ConcurrentHashMap<>();
複製代碼
原來Retrofit還擁有一個對請求方法的緩存,具體查看ServiceMethod
這個類:
package retrofit2;
import okhttp3.HttpUrl;
import ... ... ;
/** Adapts an invocation of an interface method into an HTTP call. */
final class ServiceMethod<R, T> {
// Upper and lower characters, digits, underscores, and hyphens, starting with a character.
static final String PARAM = "[a-zA-Z][a-zA-Z0-9_-]*";
... ...
private final HttpUrl baseUrl;
... ...
}
複製代碼
如今就已經找到了問題的緣由,原來每一個方法的緩存中也存在一個HttpUrl,那麼修改的時候也要將緩存中的HttpUrl替換掉。
只須要再添加代碼:
//獲取BaseUrl緩存字段serviceMethodCache
Field cacheField = retrofitClass.getDeclaredField("serviceMethodCache");
cacheField.setAccessible(true);
//獲取Retrofit對baseUrl的緩存Map
Map<Method, Object> cacheMap = (Map<Method, Object>) cacheField.get(HttpModule.RETROFIT);
if (null != cacheMap && cacheMap.size() > 0) {
//經過迭代修改map中的url,使其中的url都爲更換新的url後的httpUrl
for (Map.Entry<Method, Object> methodObjectEntry : cacheMap.entrySet()) {
Class valueClass = methodObjectEntry.getValue().getClass();
baseUrlField = valueClass.getDeclaredField("baseUrl");
baseUrlField.setAccessible(true);
baseUrlField.set(methodObjectEntry.getValue(), httpUrl);
}
}
複製代碼
在此獻上完整的修改工具類,你們只須要根據本身的框架獲取到Retrofit對象便可使用:
public static boolean hookRetrofitUrl(AboutUsActivity.Http http) {
if (http == null) {
return false;
}
try {
//獲取HttpUrl對象
Class<?> httpClass = Class.forName("okhttp3.HttpUrl");
HttpUrl httpUrl = HttpModule.RETROFIT.baseUrl();
//修改url
Field url = httpClass.getDeclaredField("url");
url.setAccessible(true);
url.set(httpUrl, http.url);
//修改host
Field host = httpClass.getDeclaredField("host");
host.setAccessible(true);
host.set(httpUrl, http.host);
//修改port端口號
Field port = httpClass.getDeclaredField("port");
port.setAccessible(true);
port.set(httpUrl, http.port);
//獲取Retrofit
Class<Retrofit> retrofitClass = Retrofit.class;
Field baseUrlField = retrofitClass.getDeclaredField("baseUrl");
//修改baseUrl
baseUrlField.setAccessible(true);
baseUrlField.set(HttpModule.RETROFIT, httpUrl);
//獲取BaseUrl緩存字段serviceMethodCache
Field cacheField = retrofitClass.getDeclaredField("serviceMethodCache");
cacheField.setAccessible(true);
//獲取Retrofit對baseUrl的緩存Map
Map<Method, Object> cacheMap = (Map<Method, Object>) cacheField.get(HttpModule.RETROFIT);
if (null != cacheMap && cacheMap.size() > 0) {
//經過迭代修改map中的url,使其中的url都爲更換新的url後的httpUrl
for (Map.Entry<Method, Object> methodObjectEntry : cacheMap.entrySet()) {
Class valueClass = methodObjectEntry.getValue().getClass();
baseUrlField = valueClass.getDeclaredField("baseUrl");
baseUrlField.setAccessible(true);
baseUrlField.set(methodObjectEntry.getValue(), httpUrl);
}
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
複製代碼
只須要將url、主機、端口號傳入便可
Http http = new Http("http://www.baidu.com/", "www.baidu.com", 80);
if (HookUtils.hookRetrofitUrl(http)) {
ToastUtils.show("請求路徑修改爲功");
} else {
ToastUtils.show("請求路徑修改失敗");
}
複製代碼
先發送一次請求,而後點擊一個按鈕修改請求路徑,查看控制檯輸出:
使用反射的方式能夠不須要修改請求的框架等地方,使反射模塊解耦出來利於代碼的易讀性,比使用攔截器稍加方便適合一點。感謝你們的閱讀,若有出入或者不足請你們及時指正,後續會將源碼和Small搭建等文章編輯發佈並上傳git。
長路漫漫,菜不是原罪,墮落纔是原罪。
個人CSDN:blog.csdn.net/wuyangyang_…
個人簡書:www.jianshu.com/u/20c2f2c35…
個人掘金:juejin.im/user/58009b…
個人GitHub:github.com/wuyang2000
我的網站:www.xiyangkeji.cn
我的app(茜茜)蒲公英鏈接:www.pgyer.com/KMdT
個人微信公衆號:茜洋 (按期推送優質技術文章,歡迎關注)
Android技術交流羣:691174792
以上文章都可轉載,轉載請註明原創。