使用Retrofit+Okhttp进行请求的项目应该挺多的,颇有可能会遇到一个需求。
就是能够动态的修改Retrofit+Okhttp框架下的请求地址(BaseUrl),这样就但是实现各类后台环境下的请求切换。
而Retrofit又没有提供一个较为方便好用的切换BaseUrl的方法,那么就要寻找别的途径来解决这个问题。java
Retrofit拦截器的主要做用在于对网络传输的数据进行拦截和处理。经过拦截器拦截即将发出的请求及对响应结果作相应处理,典型的处理方式是修改header添加一下特定的参数,如后台须要的token、deviceId、渠道号等参数。既然拦截器能够进行这些参数的修改,就也能够对请求的url进行处理。拦截器有两种:git
处理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。缓存
这个拦截器主要处理请求数据的展现,方便于调试用,须要导入拦截器的扩展包。
com.squareup.okhttp3:logging-interceptor:3.8.1
微信
要想经过反射来修改请求的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 = 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步操做:
既然没有修改为功,那确定是某些地方发生了一些不可描述的问题。
再次从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);
}
}
复制代码
在此献上完整的修改工具类,你们只须要根据本身的框架获取到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
以上文章都可转载,转载请注明原创。