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 知識梳理系列:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容