使用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:https://blog.csdn.net/wuyangyang_2000
我的简书:https://www.jianshu.com/u/20c2f2c3560a
我的掘金:https://juejin.im/user/58009b94a0bb9f00586bb8a0
我的GitHub:https://github.com/wuyang2000
个人网站:http://www.xiyangkeji.cn
个人app(茜茜)蒲公英连接:https://www.pgyer.com/KMdT
我的微信公众号:茜洋 (定期推送优质技术文章,欢迎关注)
Android技术交流群:691174792
以上文章均可转载,转载请注明原创。