背景
最近在业务上发生了一些问题,解决问题之后,被要求做域名容灾。
当然了,app单独做域名容灾是不现实的,一定要配合服务端完成,app本地需要实现的就是拿到新的域名,要能不通过发版就动态切换。
借鉴
使用的是RetrofitUrlManager框架
中文介绍在这里,里面的使用场景跟我的业务很相像
https://www.jianshu.com/p/2919bdb8d09a
贴出项目地址
https://github.com/JessYanCoding/RetrofitUrlManager
接入
我项目的网络方案也是okhttp+retrofit+rxjava2
retrofit支持相对路径,简直太棒。
目前我现在只需要简单模式,RetrofitUrlManager的接入就很简单了。
业务流程
![](https://img.haomeiwen.com/i2554175/15017245747f8632.png)
源码阅读
从初始化开始。一行代码实现,可以看到,RetrofitUrlManager就是对okhttpClient的再一次封装
OkHttpClient = RetrofitUrlManager.getInstance().with(new OkHttpClient.Builder()).build();
编译期就知道了依赖
static {
boolean hasDependency;
try {
Class.forName("okhttp3.OkHttpClient");
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
DEPENDENCY_OKHTTP = hasDependency;
}
//单例调用,关键就是利用拦截器
private RetrofitUrlManager() {
if (!DEPENDENCY_OKHTTP) { //使用本框架必须依赖 Okhttp
throw new IllegalStateException("Must be dependency Okhttp");
}
UrlParser urlParser = new DefaultUrlParser();
urlParser.init(this);
setUrlParser(urlParser);
this.mInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
if (!isRun()) // 可以在 App 运行时, 随时通过 setRun(false) 来结束本框架的运行
return chain.proceed(chain.request());
return chain.proceed(processRequest(chain.request()));
}
};
}
private static class RetrofitUrlManagerHolder {
private static final RetrofitUrlManager INSTANCE = new RetrofitUrlManager();
}
//静态法懒汉式单例
public static final RetrofitUrlManager getInstance() {
return RetrofitUrlManagerHolder.INSTANCE;
}
/**
* 将 {@link OkHttpClient.Builder} 传入, 配置一些本框架需要的参数
*/
public OkHttpClient.Builder with(OkHttpClient.Builder builder) {
checkNotNull(builder, "builder cannot be null");
return builder
.addInterceptor(mInterceptor);
}
/**
* 对 {@link Request} 进行一些必要的加工, 执行切换 BaseUrl 的相关逻辑
*/
public Request processRequest(Request request) {
if (request == null) return request;
Request.Builder newBuilder = request.newBuilder();
String url = request.url().toString();
//如果 Url 地址中包含 IDENTIFICATION_IGNORE 标识符, 框架将不会对此 Url 进行任何切换 BaseUrl 的操作
if (url.contains(IDENTIFICATION_IGNORE)) {
return pruneIdentification(newBuilder, url);
}
//从retrofit的header里找到domain标记,一个header里只能有一个domain
String domainName = obtainDomainNameFromHeaders(request);
HttpUrl httpUrl;
//找到已经设置的listener,并放入一个数组里
Object[] listeners = listenersToArray();
// 如果有 header,获取 header 中 domainName 所映射的 url,若没有,则检查全局的 BaseUrl,未找到则为null
if (!TextUtils.isEmpty(domainName)) {
notifyListener(request, domainName, listeners);
httpUrl = fetchDomain(domainName);//hashMap的方法,通过domain这个key取出相应的url value
newBuilder.removeHeader(DOMAIN_NAME);//为什么要删除这个header呢?
} else {
notifyListener(request, GLOBAL_DOMAIN_NAME, listeners);
httpUrl = getGlobalDomain();//hashMap允许null的value
}
if (null != httpUrl) {
//解析该url
HttpUrl newUrl = mUrlParser.parseUrl(httpUrl, request.url());
if (debug)
Log.d(RetrofitUrlManager.TAG, "The new url is { " + newUrl.toString() + " }, old url is { " + request.url().toString() + " }");
if (listeners != null) {
for (int i = 0; i < listeners.length; i++) {
((onUrlChangeListener) listeners[i]).onUrlChanged(newUrl, request.url()); // 通知监听器此 Url 的 BaseUrl 已被切换
}
}
return newBuilder
.url(newUrl)
.build();
}
//如果global url也为空,则不加工该url
return newBuilder.build();
}
在一开始就manager就初始化了一个urlParser,是一个DefaultUrlParser
https://github.com/JessYanCoding/RetrofitUrlManager/blob/master/manager/src/main/java/me/jessyan/retrofiturlmanager/parser/DefaultUrlParser.java
这个DefaultUrlParser功能很简单,饿汉式初始化了一个DomainUrlParser,并且在parseUrl时根据关键字和设置值,调用相应的UrlParser,但是不处理parser逻辑,而是进行转发,简单的说DefaultUrlParser就是一个url转发器,真的url处理放在mDomainUrlParser,mAdvancedUrlParser,mSuperUrlParser。
public void init(RetrofitUrlManager retrofitUrlManager) {
this.mRetrofitUrlManager = retrofitUrlManager;
this.mDomainUrlParser = new DomainUrlParser();
this.mDomainUrlParser.init(retrofitUrlManager);
}
@Override
public HttpUrl parseUrl(HttpUrl domainUrl, HttpUrl url) {
if (null == domainUrl) return url;
if (url.toString().contains(IDENTIFICATION_PATH_SIZE)) {
if (mSuperUrlParser == null) {
synchronized (this) {
if (mSuperUrlParser == null) {
mSuperUrlParser = new SuperUrlParser();
mSuperUrlParser.init(mRetrofitUrlManager);
}
}
}
return mSuperUrlParser.parseUrl(domainUrl, url);
}
//如果是高级模式则使用高级解析器
if (mRetrofitUrlManager.isAdvancedModel()) {
if (mAdvancedUrlParser == null) {
synchronized (this) {
if (mAdvancedUrlParser == null) {
mAdvancedUrlParser = new AdvancedUrlParser();
mAdvancedUrlParser.init(mRetrofitUrlManager);
}
}
}
return mAdvancedUrlParser.parseUrl(domainUrl, url);
}
return mDomainUrlParser.parseUrl(domainUrl, url);
}
我们这里只分析DomainUrlParser
https://github.com/JessYanCoding/RetrofitUrlManager/blob/master/manager/src/main/java/me/jessyan/retrofiturlmanager/parser/DomainUrlParser.java
其余的两个,是对更复杂的域名替换业务增加相应的逻辑处理。
public HttpUrl parseUrl(HttpUrl domainUrl, HttpUrl url) {
// 如果 HttpUrl.parse(url); 解析为 null 说明,url 格式不正确,正确的格式为 "https://github.com:443"
// http 默认端口 80, https 默认端口 443, 如果端口号是默认端口号就可以将 ":443" 去掉
// 只支持 http 和 https
if (null == domainUrl) return url;
HttpUrl.Builder builder = url.newBuilder();//新建了一个HttpUrl.Builder专门处理url
if (TextUtils.isEmpty(mCache.get(getKey(domainUrl, url)))) {//缓存里没有
for (int i = 0; i < url.pathSize(); i++) {
//当删除了上一个 index, PathSegment 的 item 会自动前进一位, 所以 remove(0) 就好
builder.removePathSegment(0);
}
List<String> newPathSegments = new ArrayList<>();
newPathSegments.addAll(domainUrl.encodedPathSegments());
newPathSegments.addAll(url.encodedPathSegments());
for (String PathSegment : newPathSegments) {
builder.addEncodedPathSegment(PathSegment);
}
} else {//缓存里有,则直接处理路径
builder.encodedPath(mCache.get(getKey(domainUrl, url)));
}
HttpUrl httpUrl = builder
.scheme(domainUrl.scheme())
.host(domainUrl.host())
.port(domainUrl.port())
.build();
//处理完这一次,放入缓存
if (TextUtils.isEmpty(mCache.get(getKey(domainUrl, url)))) {
mCache.put(getKey(domainUrl, url), httpUrl.encodedPath());
}
return httpUrl;
}
private String getKey(HttpUrl domainUrl, HttpUrl url) {
return domainUrl.encodedPath() + url.encodedPath();
}
到这里整个源码就基本完成了,可以看出来,整个框架就是利用okhttp的拦截器,在结合retrofit灵活的header完成域名的切换。
疑问
1.manager的registerUrlChangeListener和unregisterUrlChangeListener方法是什么时候调用的,又是谁去调用的?
2.在processRequest方法内,
notifyListener(request, domainName, listeners);
httpUrl = fetchDomain(domainName);//hashMap的方法,通过domain这个key取出相应的url value
newBuilder.removeHeader(DOMAIN_NAME);
}
为什么要调用listeners的onUrlChangeBefore方法呢?之后又为什么删除这个header呢?这样做的好处是什么?
尝试解答
在demo里MainActivity类有个初始化方法,调用了registerUrlChangeListener
,里面的listener我的理解是类似xposed框架的beforeHookedMethod和afterHookedMethod
private void initListener() {
this.mListener = new ChangeListener();
//如果有需要可以注册监听器,当一个 Url 的 BaseUrl 被新的 Url 替代,则会回调这个监听器,调用时间是在接口请求服务器之前
RetrofitUrlManager.getInstance().registerUrlChangeListener(mListener);
....
}
private class ChangeListener implements onUrlChangeListener {
@Override
public void onUrlChangeBefore(HttpUrl oldUrl, String domainName) {
Log.d("MainActivity", String.format("The oldUrl is <%s>, ready fetch <%s> from DomainNameHub",
oldUrl.toString(),
domainName));
}
@Override
public void onUrlChanged(final HttpUrl newUrl, HttpUrl oldUrl) {
Observable.just(1)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
Toast.makeText(getApplicationContext(), "The newUrl is { " + newUrl.toString() + " }", Toast.LENGTH_LONG).show();
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
throwable.printStackTrace();
}
});
}
}
notify就是为了进行通知,demo里在onUrlChangeBefore里只是为了打log。
可是之后又为什么删除这个header呢??
可能是避免递归处理了吧
来自作者的回复
https://github.com/JessYanCoding/RetrofitUrlManager/issues/23
![](https://img.haomeiwen.com/i2554175/1b939ca2c2f482f2.png)
网友评论