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

以上文章都可转载,转载请注明原创。

相关文章
相关标签/搜索