阅读 183

Retrofit+OkHttp3反射动态修改请求路径

前言

使用Retrofit+Okhttp进行请求的项目应该挺多的,很有可能会遇到一个需求。
就是可以动态的修改Retrofit+Okhttp框架下的请求地址(BaseUrl),这样就可是实现各种后台环境下的请求切换。
而Retrofit又没有提供一个较为方便好用的切换BaseUrl的方法,那么就要寻找别的途径来解决这个问题。

一、Retrofit拦截器进行HttpUrl重构

  Retrofit拦截器的主要作用在于对网络传输的数据进行拦截和处理。通过拦截器拦截即将发出的请求及对响应结果做相应处理,典型的处理方式是修改header添加一下特定的参数,如后台需要的token、deviceId、渠道号等参数。既然拦截器可以进行这些参数的修改,就也可以对请求的url进行处理。拦截器有两种:

1、Interceptor

处理header等参数可以在Interceptor中处理,创建Interceptor的对象,其提供了一个方法intercept(Chain chain)
其中chain对象就可以拿到请求的request,然后进行一些处理。

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。

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。

2、HttpLoggingInterceptor

这个拦截器主要处理请求数据的展示,方便于调试用,需要导入拦截器的扩展包。
com.squareup.okhttp3:logging-interceptor:3.8.1

二、通过反射对Retrofit BaseUrl进行重构

1、反射的切入点

要想通过反射来修改请求的BaseUrl,首先需要了解修改的字段是那些,在什么地方。所以需要对Retrofit的源码进行查看:
Retrofit是通过Build去构建请求参数的:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("请求的url")
... ...
复制代码

所以.baseUrl()方式就是切入点,查看其代码的实现:

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又是什么对象呢?查看其源码:

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;
复制代码

看到这三个字段,我们完全找到了反射所需要的切入点,只需要通过反射修改这三个字段即可。

2、反射修改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的修改,但是经过测试发现请求路径还是原路径。这是为什么呢?

3、对Retrofit请求方法的缓存进行修改

既然没有修改成功,那肯定是某些地方发生了一些不可描述的问题。
再次从Retrofit进行梳理,请大家浏览一下 1、反射的切入点 第三个代码片段,可以看到这样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);
      }
}
复制代码

三、修改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;
        }
    }
复制代码

四、使用与测试

1、使用

只需要将url、主机、端口号传入即可

Http http = new Http("http://www.baidu.com/", "www.baidu.com", 80);
if (HookUtils.hookRetrofitUrl(http)) {
     ToastUtils.show("请求路径修改成功");
} else {
     ToastUtils.show("请求路径修改失败");
}
复制代码

2、调试

先发送一次请求,然后点击一个按钮修改请求路径,查看控制台输出:

在这里插入图片描述

总结

  使用反射的方式可以不需要修改请求的框架等地方,使反射模块解耦出来利于代码的易读性,比使用拦截器稍加方便适合一点。感谢大家的阅读,如有出入或者不足请大家及时指正,后续会将源码和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

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


关注下面的标签,发现更多相似文章
评论