RxJava2 实战知识梳理(14) - 在 token 过期时,刷新过期 token 并重新发起请求

RxJava2 实战系列文章

RxJava2 实战知识梳理(1) - 后台执行耗时操作,实时通知 UI 更新
RxJava2 实战知识梳理(2) - 计算一段时间内数据的平均值
RxJava2 实战知识梳理(3) - 优化搜索联想功能
RxJava2 实战知识梳理(4) - 结合 Retrofit 请求新闻资讯
RxJava2 实战知识梳理(5) - 简单及进阶的轮询操作
RxJava2 实战知识梳理(6) - 基于错误类型的重试请求
RxJava2 实战知识梳理(7) - 基于 combineLatest 实现的输入表单验证
RxJava2 实战知识梳理(8) - 使用 publish + merge 优化先加载缓存,再读取网络数据的请求过程
RxJava2 实战知识梳理(9) - 使用 timer/interval/delay 实现任务调度
RxJava2 实战知识梳理(10) - 屏幕旋转导致 Activity 重建时恢复任务
RxJava2 实战知识梳理(11) - 检测网络状态并自动重试请求
RxJava2 实战知识梳理(12) - 实战讲解 publish & replay & share & refCount & autoConnect
RxJava2 实战知识梳理(13) - 如何使得错误发生时不自动停止订阅关系
RxJava2 实战知识梳理(14) - 在 token 过期时,刷新过期 token 并重新发起请求
RxJava2 实战知识梳理(15) - 实现一个简单的 MVP + RxJava + Retrofit 应用


一、应用背景

首先要感谢简友 楠柯壹梦 提供的实战案例,这篇文章的例子是基于他提出的需要在token失效时,刷新token并重新请求接口的应用场景所想到的解决方案。如果大家有别的案例或者在实际中遇到什么问题也可以私信我,让我们一起完善这系列的文章。

有时候,我们的某些接口会依赖于用户的token信息,像我们项目当中的资讯评论列表、或者账户的书签同步都会依赖于用户token信息,但是token往往会有一定的有效期,那么我们在请求这些接口返回token失效的时候,就需要刷新token再重新发起一次请求,这个流程图可以归纳如下:


这个应用的场景和 RxJava2 实战知识梳理(6) - 基于错误类型的重试请求 中介绍的场景很类似,之前提到的错误类型就指的是token失效,但是相比之前的例子,我们增加了额外的两个需求:

  • 在重试之前,需要先去刷新一次token,而不是单纯地等待一段时间再重试。
  • 如果有多个请求都出现了因token失效而需要重新刷新token的情况,那么需要判断当前是否有另一个请求正在刷新token,如果有,那么就不要发起刷新token的请求,而是等待刷新token的请求返回后,直接进行重试。

本文的代码可以通过 RxSample 的第十四章获取。

二、示例讲解

2.1 Token 存储模块

首先,我们需要一个地方来缓存需要的Token,这里用SharedPreferences来实现,有想了解其内部实现原理的同学可以看这篇文章:Android 数据存储知识梳理(3) - SharedPreference 源码解析

public class Store {

    private static final String SP_RX = "sp_rx";
    private static final String TOKEN = "token";

    private SharedPreferences mStore;

    private Store() {
        mStore = Utils.getAppContext().getSharedPreferences(SP_RX, Context.MODE_PRIVATE);
    }

    public static Store getInstance() {
        return Holder.INSTANCE;
    }

    private static final class Holder {
        private static final Store INSTANCE = new Store();
    }

    public void setToken(String token) {
        mStore.edit().putString(TOKEN, token).apply();
    }

    public String getToken() {
        return mStore.getString(TOKEN, "");
    }
}

2.2 依赖于 token 的接口

这里,我们用一个简单的getUserObservable来模拟依赖于token的接口,token存储的是获取的时间,为了演示方便,我们设置如果距离上次获取的时间大于2s,那么就认为过期,并抛出token失效的错误,否则调用onNext方法返回接口给下游。

    private Observable<String> getUserObservable (final int index, final String token) {
        return Observable.create(new ObservableOnSubscribe<String>() {

            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {
                Log.d(TAG, index + "使用token=" + token + "发起请求");
                //模拟根据Token去请求信息的过程。
                if (!TextUtils.isEmpty(token) && System.currentTimeMillis() - Long.valueOf(token) < 2000) {
                    e.onNext(index + ":" + token + "的用户信息");
                } else {
                    e.onError(new Throwable(ERROR_TOKEN));
                }
            }
        });
    }

2.3 完整的请求过程

下面,我们来看一下整个完整的请求过程:

   private void startRequest(final int index) {
        Observable<String> observable = Observable.defer(new Callable<ObservableSource<String>>() {
            @Override
            public ObservableSource<String> call() throws Exception {
                String cacheToken = TokenLoader.getInstance().getCacheToken();
                Log.d(TAG, index + "获取到缓存Token=" + cacheToken);
                return Observable.just(cacheToken);
            }
        }).flatMap(new Function<String, ObservableSource<String>>() {
            @Override
            public ObservableSource<String> apply(String token) throws Exception {
                return getUserObservable(index, token);
            }
        }).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {

            private int mRetryCount = 0;

            @Override
            public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {
                return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {

                    @Override
                    public ObservableSource<?> apply(Throwable throwable) throws Exception {
                        Log.d(TAG, index + ":" + "发生错误=" + throwable + ",重试次数=" + mRetryCount);
                        if (mRetryCount > 0) {
                            return Observable.error(new Throwable(ERROR_RETRY));
                        } else if (ERROR_TOKEN.equals(throwable.getMessage())) {
                            mRetryCount++;
                            return TokenLoader.getInstance().getNetTokenLocked();
                        } else {
                            return Observable.error(throwable);
                        }
                    }
                });
            }
        });
        DisposableObserver<String> observer = new DisposableObserver<String>() {

            @Override
            public void onNext(String value) {
                Log.d(TAG, index + ":" + "收到信息=" + value);
            }

            @Override
            public void onError(Throwable e) {
                Log.d(TAG, index + ":" + "onError=" + e);
            }

            @Override
            public void onComplete() {
                Log.d(TAG, index + ":" + "onComplete");
            }
        };
        observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(observer);
    }

为了方便大家阅读,我把所有的逻辑都写在了一整个调用链里,整个调用链分为四个部分:

  • defer:读取缓存中的token信息,这里调用了TokenLoader中读取缓存的接口,而这里使用defer操作符,是为了在重订阅时,重新创建一个新的Observable,以读取最新的缓存token信息,其原理图如下:
    defer 原理图
  • flatMap:通过token信息,请求必要的接口。
  • retryWhen:使用重订阅的方式来处理token失效时的逻辑,这里分为三种情况:重试次数到达,那么放弃重订阅,直接返回错误;请求token接口,根据token请求的结果决定是否重订阅;其它情况直接放弃重订阅。
  • subscribe:返回接口数据。

2.4 TokenLoader 的实现

关键点在于TokenLoader的实现逻辑,代码如下:

public class TokenLoader {

    private static final String TAG = TokenLoader.class.getSimpleName();

    private AtomicBoolean mRefreshing = new AtomicBoolean(false);
    private PublishSubject<String> mPublishSubject;
    private Observable<String> mTokenObservable;

    private TokenLoader() {
        mPublishSubject = PublishSubject.create();
        mTokenObservable = Observable.create(new ObservableOnSubscribe<String>() {
            @Override
            public void subscribe(ObservableEmitter<String> e) throws Exception {
                Thread.sleep(1000);
                Log.d(TAG, "发送Token");
                e.onNext(String.valueOf(System.currentTimeMillis()));
            }
        }).doOnNext(new Consumer<String>() {
            @Override
            public void accept(String token) throws Exception {
                Log.d(TAG, "存储Token=" + token);
                Store.getInstance().setToken(token);
                mRefreshing.set(false);
            }
        }).doOnError(new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                mRefreshing.set(false);
            }
        }).subscribeOn(Schedulers.io());
    }

    public static TokenLoader getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final TokenLoader INSTANCE = new TokenLoader();
    }

    public String getCacheToken() {
        return Store.getInstance().getToken();
    }

    public Observable<String> getNetTokenLocked() {
        if (mRefreshing.compareAndSet(false, true)) {
            Log.d(TAG, "没有请求,发起一次新的Token请求");
            startTokenRequest();
        } else {
            Log.d(TAG, "已经有请求,直接返回等待");
        }
        return mPublishSubject;
    }

    private void startTokenRequest() {
        mTokenObservable.subscribe(mPublishSubject);
    }

}

retryWhen中,我们调用了getNetTokenLocked来获得一个PublishSubject,为了实现前面说到的下面这个逻辑:


我们使用了一个AtomicBoolean来标记是否有刷新Token的请求正在执行,如果有,那么直接返回一个PublishSubject,否则就先发起一次刷新token的请求,并将PublishSubject作为该请求的订阅者。

这里用到了PublishSubject的特性,它既是作为Token请求的订阅者,同时又作为retryWhen函数所返回Observable的发送方,因为retryWhen返回的Observable所发送的值就决定了是否需要重订阅:

  • 如果Token请求返回正确,那么就会发送onNext事件,触发重订阅操作,使得我们可以再次触发一次重试操作。
  • 如果Token请求返回错误,那么就会放弃重订阅,使得整个请求的调用链结束。

AtomicBoolean保证了多线程的情况下,只能有一个刷新Token的请求,在这个阶段内不会触发重复的刷新token请求,仅仅是作为观察者而已,并且可以在刷新token的请求回来之后立刻进行重订阅的操作。在doOnNext/doOnError中,我们将正在刷新的标志位恢复,同时缓存最新的token

为了模拟上面提到的多线程请求刷新token的情况,我们在发起一个请求500ms之后,立刻发起另一个请求,当第二个请求决定是否要重订阅时,第一个请求正在进行刷新token的操作。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_token);
        mBtnRequest = (Button) findViewById(R.id.bt_request);
        mBtnRequest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startRequest(0);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                startRequest(1);
            }
        });
    }

控制台的输出如下,可以看到在第二个请求决定是否要重订阅时,它判断到已经有请求,因此只是等待而已。而在第一个请求导致的token刷新回调之后,两个请求都进行了重试,并成功地请求到了接口信息。

2.5 操作符

本文中用到的操作符的官方解释链接如下:

关于retryWhen的更详细的解释,推荐大家可以看一下之前的 RxJava2 实战知识梳理(6) - 基于错误类型的重试请求,它是这篇文章的基础。


更多文章,欢迎访问我的 Android 知识梳理系列:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容