一、應用背景
首先要感謝簡友 楠柯壹夢 提供的實戰案例,這篇文章的例子是基於他提出的需要在token
失效時,重新整理token
並重新請求介面的應用場景所想到的解決方案。如果大家有別的案例或者在實際中遇到什麼問題也可以私信我,讓我們一起完善這系列的文章。
有時候,我們的某些介面會依賴於使用者的token
資訊,像我們專案當中的資訊評論列表、或者賬戶的書籤同步都會依賴於使用者token
資訊,但是token
往往會有一定的有效期,那麼我們在請求這些介面返回token
失效的時候,就需要重新整理token
再重新發起一次請求,這個流程圖可以歸納如下:
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
資訊,其原理圖如下: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 知識梳理系列:
- Android 知識梳理目錄:www.jianshu.com/p/fd82d1899…
- 個人主頁:lizejun.cn
- 個人知識總結目錄:lizejun.cn/categories/