使用Retrofit+Okhttp进行请求的项目应该挺多的,很有可能会遇到一个需求。
就是可以动态的修改Retrofit+Okhttp框架下的请求地址(BaseUrl),这样就可是实现各种后台环境下的请求切换。
而Retrofit又没有提供一个较为方便好用的切换BaseUrl的方法,那么就要寻找别的途径来解决这个问题。
Retrofit拦截器的主要作用在于对网络传输的数据进行拦截和处理。通过拦截器拦截即将发出的请求及对响应结果做相应处理,典型的处理方式是修改header添加一下特定的参数,如后台需要的token、deviceId、渠道号等参数。既然拦截器可以进行这些参数的修改,就也可以对请求的url进行处理。拦截器有两种:
处理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。
这个拦截器主要处理请求数据的展示,方便于调试用,需要导入拦截器的扩展包。
com.squareup.okhttp3:logging-interceptor:3.8.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; 复制代码
看到这三个字段,我们完全找到了反射所需要的切入点,只需要通过反射修改这三个字段即可。
首先我们需要获取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进行梳理,请大家浏览一下 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); } } 复制代码
在此献上完整的修改工具类,大家只需要根据自己的框架获取到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
以上文章均可转载,转载请注明原创。