Retrofit+OkHttp3反射動態修改請求路徑

前言

使用Retrofit+Okhttp進行請求的項目應該挺多的,頗有可能會遇到一個需求。
就是能夠動態的修改Retrofit+Okhttp框架下的請求地址(BaseUrl),這樣就但是實現各類後臺環境下的請求切換。
而Retrofit又沒有提供一個較爲方便好用的切換BaseUrl的方法,那麼就要尋找別的途徑來解決這個問題。java

1、Retrofit攔截器進行HttpUrl重構

  Retrofit攔截器的主要做用在於對網絡傳輸的數據進行攔截和處理。經過攔截器攔截即將發出的請求及對響應結果作相應處理,典型的處理方式是修改header添加一下特定的參數,如後臺須要的token、deviceId、渠道號等參數。既然攔截器能夠進行這些參數的修改,就也能夠對請求的url進行處理。攔截器有兩種:git

一、Interceptor

處理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。緩存

二、HttpLoggingInterceptor

這個攔截器主要處理請求數據的展現,方便於調試用,須要導入攔截器的擴展包。
com.squareup.okhttp3:logging-interceptor:3.8.1微信

2、經過反射對Retrofit BaseUrl進行重構

一、反射的切入點

要想經過反射來修改請求的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 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請求方法的緩存進行修改

既然沒有修改爲功,那確定是某些地方發生了一些不可描述的問題。
再次從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);
      }
}
複製代碼

3、修改Retrofit2+Okhttp3的BaseUrl

在此獻上完整的修改工具類,你們只須要根據本身的框架獲取到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;
        }
    }
複製代碼

4、使用與測試

一、使用

只須要將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

以上文章都可轉載,轉載請註明原創。

相關文章
相關標籤/搜索