美文网首页Android-Rxjava&retrofit&daggerandroid
动态切换域名框架RetrofitUrlManager源码阅读

动态切换域名框架RetrofitUrlManager源码阅读

作者: 普通的程序员 | 来源:发表于2018-12-23 22:09 被阅读55次

背景

最近在业务上发生了一些问题,解决问题之后,被要求做域名容灾。

当然了,app单独做域名容灾是不现实的,一定要配合服务端完成,app本地需要实现的就是拿到新的域名,要能不通过发版就动态切换。

借鉴

使用的是RetrofitUrlManager框架
中文介绍在这里,里面的使用场景跟我的业务很相像
https://www.jianshu.com/p/2919bdb8d09a
贴出项目地址
https://github.com/JessYanCoding/RetrofitUrlManager

接入

我项目的网络方案也是okhttp+retrofit+rxjava2
retrofit支持相对路径,简直太棒。
目前我现在只需要简单模式,RetrofitUrlManager的接入就很简单了。

业务流程

我的业务流程图

源码阅读

从初始化开始。一行代码实现,可以看到,RetrofitUrlManager就是对okhttpClient的再一次封装

 OkHttpClient = RetrofitUrlManager.getInstance().with(new OkHttpClient.Builder()).build();

查看该类https://github.com/JessYanCoding/RetrofitUrlManager/blob/master/manager/src/main/java/me/jessyan/retrofiturlmanager/RetrofitUrlManager.java

编译期就知道了依赖
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

作者的回复

相关文章

网友评论

    本文标题:动态切换域名框架RetrofitUrlManager源码阅读

    本文链接:https://www.haomeiwen.com/subject/usfokqtx.html