一引文
上一篇文章筆者通過自己的怕坑經歷闡述了全域性API錯誤統一處理的兩種方式(自定義解析器方式和flatMap改變流走向方式)和 無感更新token的兩種方式(動態代理+retryWhen方式和onErrorResumeNext操作符+retryWhen操作符),並且討論了無感更新token的兩種方式使用onErrorResumeNext操作符+retryWhen操作符會更加靈活。並且上一篇文章遺留下一個需求和一個問題:
遺留需求:token失效去跳轉登入介面,登入成功返回原來頁面繼續請求,登入失敗停留再登入頁面,點選返回,返回原來介面,但是不去請求。 (建設銀行手機app就是這樣的實現)
遺留問題: 併發請求會出現token重新整理介面多次請求問題
二解決上篇遺留需求
開始解決之前需要大家去看一片文章,學習如何避免使用onActivityResult。關於理論的講解文章講解的很清楚,原理和和RxPermissions實現方式一樣,不用寫回撥的方式一樣(去新增一個隱藏的Fragment)我這裡就不做過多的闡述。最後的程式碼如下:
沒有檢視的Fragment程式碼
public class AvoidOnResultFragment extends Fragment {
private Map<Integer, PublishSubject<ActivityResultInfo>> mSubjects = new HashMap<>();
public AvoidOnResultFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
//當前intent傳遞到這個Fragment中
public Observable<ActivityResultInfo> startForResult(final Intent intent) {
//建立一個上流
final PublishSubject<ActivityResultInfo> subject = PublishSubject.create();
//當這個流訂閱的時候,我需要做的是
return subject.doOnSubscribe(new Consumer<Disposable>() {
@Override
public void accept(Disposable disposable) throws Exception {
//把對應的回撥儲存起來
mSubjects.put(subject.hashCode(), subject);
//開始跳轉
startActivityForResult(intent, subject.hashCode());
}
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//移除回撥,同時返回這個回撥
PublishSubject<ActivityResultInfo> subject = mSubjects.remove(requestCode);
if (subject != null) {
//把獲取的結果傳送成流,傳遞下去
subject.onNext(new ActivityResultInfo(resultCode, data));
//最後呼叫完成流
subject.onComplete();
}
}
}複製程式碼
對外暴漏的使用方法:
public class AvoidOnResult {
private static final String TAG = "AvoidOnResult";
private AvoidOnResultFragment mAvoidOnResultFragment;
public AvoidOnResult(Activity activity) {
//獲取當前activity中是否有已經新增的fragment
mAvoidOnResultFragment = getAvoidOnResultFragment(activity);
}
//如果傳遞fragment,獲取當前activity,然後繼續給fragment掛載的activity新增無檢視的fragment
public AvoidOnResult(Fragment fragment) {
this(fragment.getActivity());
}
private AvoidOnResultFragment getAvoidOnResultFragment(Activity activity) {
AvoidOnResultFragment avoidOnResultFragment = findAvoidOnResultFragment(activity);
//如果沒有,那麼就建立一個fragment新增進去
if (avoidOnResultFragment == null) {
avoidOnResultFragment = new AvoidOnResultFragment();
FragmentManager fragmentManager = activity.getFragmentManager();
fragmentManager
.beginTransaction()
.add(avoidOnResultFragment, TAG)
.commitAllowingStateLoss();
fragmentManager.executePendingTransactions();
}
return avoidOnResultFragment;
}
private AvoidOnResultFragment findAvoidOnResultFragment(Activity activity) {
//獲取當前activity中是否有已經新增的fragment
return (AvoidOnResultFragment) activity.getFragmentManager().findFragmentByTag(TAG);
}
//跳轉:把這個Intent傳遞到新增的Fragment中去,返回一個Observable物件
public Observable<ActivityResultInfo> startForResult(Intent intent) {
return mAvoidOnResultFragment.startForResult(intent);
}
//過載方法:直接傳遞類的位元組碼,原因是不需要給裡邊傳遞引數
public Observable<ActivityResultInfo> startForResult(Class<?> clazz) {
Intent intent = new Intent(mAvoidOnResultFragment.getActivity(), clazz);
return startForResult(intent);
}
}複製程式碼
對應的Bean類
public class ActivityResultInfo {
private int resultCode;
private Intent data;
public ActivityResultInfo(int resultCode, Intent data) {
this.resultCode = resultCode;
this.data = data;
}
public int getResultCode() {
return resultCode;
}
public void setResultCode(int resultCode) {
this.resultCode = resultCode;
}
public Intent getData() {
return data;
}
public void setData(Intent data) {
this.data = data;
}
}複製程式碼
當你學會了消除onActivityResult。我想你大概就知道怎麼去解決這個跳轉登入的問題,原理和非同步去請求自動重新整理token一樣,只不過這裡變成了去跳轉頁面,都是非同步的。所以不會存在問題,程式碼如下:
public static <T> ObservableTransformer<T, T> tokenErrorHandlerJumpLogin(Activity activity) {
return upstream ->
upstream
.observeOn(AndroidSchedulers.mainThread())
.onErrorResumeNext(new Function<Throwable, ObservableSource<? extends T>>() {
@Override
public ObservableSource<? extends T> apply(Throwable throwable) throws Exception {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == 8) {
//這裡非同步去請求,然後再確定返回值
return new AvoidOnResult(activity)
.startForResult(LoginActivity.class)
.filter(it -> it.getResultCode() == Activity.RESULT_OK)
.flatMap(it -> {
boolean loginSucceeds = it.getData().getBooleanExtra("loginSucceed", false);
if (loginSucceeds) {
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
} else {
return Observable.error(throwable);
}
});
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}
})
.observeOn(Schedulers.io())
.retryWhen(throwableObservable -> {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == -999) {
return Observable.just(1);
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}
});
});
}複製程式碼
這裡有兩點需要去注意:
- 跳轉登入,需要切換到主執行緒中
- 再次請求,需要切換到子執行緒中
至於為什麼我目前說不上來,不過不切換會報錯。
三.解決併發多次請求重新整理token介面
上一篇已經簡單分析過這個問題存在原因。這篇我們就去解決這個併發問題。當我發現有併發問題的時候,筆者也是無從下手,因為每個請求都是獨立去請求的。後臺再梅老闆的提醒下去看了一下 Hot Observable 和 cold Observable .
初步瞭解了Hot Observable之後,感覺能解決這個併發問題,又感覺不能解決這個併發問題。後來又深入瞭解了Hot Observable之後,認為它是不能解決這個併發問題。
此時的我陷入了絕望。甚至有點懷疑這樣處理token解決不了併發問題。但是筆者沒有放棄,一遍遍的梳理需求,終於解決了這個併發問題。
需求分析:
- 控制重新整理token介面只請求一次;
先實現第一個需求,我們可以新增一個靜態boolean標記,預設是true,然後在第一次走到再次請求的程式碼之後我們首先設定成false,直到新的token到來之後再次設定成true。
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == 8) {
if (isqurst) {
isqurst = false;
return RetrofitUtil.
getInstance()
.create(API.class)
.Login("wangyong", "111111")
.flatMap(loginBean -> {
SPUtils.saveString("token", loginBean.getData().getToken());
isqurst = true;
//這裡建立一個新流去return,保證了先去請求token,之後再去重複訂閱
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});
} else {
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));//錯誤 }複製程式碼
這樣就能保證併發的時候,誰先到就去走請求token的需求,當token獲取到之後,再次設定成true。從而保證了重新整理token介面只請求一次。
那麼這樣寫問題就來了,後到的請求錯誤,因為isqurst設定成false,導致直接return,進而又去遞迴的呼叫再次請求。這樣雖然解決了token重新整理介面請求一次,但是卻有導致其他併發介面請求很多次,伺服器壓力一樣沒有減少。
這個時候需求轉換為:只要這個重新整理介面請求開始,其他併發介面都去等待新的token到來再去重試請求。
那麼怎麼才能讓他去等待呢?
我想到了阻塞訊息佇列,似乎Handler原始碼就有一個阻塞訊息佇列的實現方式。我視乎看到的勝利的曙光。想到之後我就否定了,我要用純正的Rx方式去解決,顯得正統~~
然後我就突然想到了Rx的輪詢操作符interval 和過濾操作符filter視乎可以解決這個問題。
於是乎就有了下邊的程式碼
return Observable.interval(50, TimeUnit.MILLISECONDS)
.flatMap(it -> Observable.just(isqurst))
.filter(o -> isqurst)
.flatMap(o -> {
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});複製程式碼
我間隔50毫秒的時間去發射一次,然後再根據標記去判斷是否過濾掉,就可以實現類似阻塞的效果。於是就有了下邊程式碼:
public static <T> ObservableTransformer<T, T> tokenErrorHandler() {
return upstream ->
upstream.onErrorResumeNext(throwable -> {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == 8) {
//這裡非同步去請求,然後再確定返回值,獲取併發的時間差
if (isqurst) {
isqurst = false;
return RetrofitUtil.
getInstance()
.create(API.class)
.Login("wangyong", "111111")
.flatMap(loginBean -> {
SPUtils.saveString("token", loginBean.getData().getToken());
isqurst = true;
//這裡建立一個新流去return,保證了先去請求token,之後再去重複訂閱
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});
} else {
return Observable.interval(50, TimeUnit.MILLISECONDS)
.flatMap(it -> Observable.just(isqurst))
.filter(o -> isqurst)
.flatMap(o -> {
Log.e("rrrrrrrr", "eeeeeeeeeeee");
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});
}
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}).retryWhen(throwableObservable -> {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == -999) {
return Observable.just(1);
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}
});
});
}複製程式碼
迫不及待的去測試,測試結果:又發現token介面請求有時候一次有時候多次。又梳理了一次程式碼,發現需要在這個邏輯判斷的地方加上一個同步鎖。因為CPU有可能進入了if判斷,還沒有把標記設定成false,就又讓另外一個請求進入if判斷。新增同步鎖之後的程式碼如下:
public static <T> ObservableTransformer<T, T> tokenErrorHandler() {
return upstream ->
upstream.onErrorResumeNext(throwable -> {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == 8) {
synchronized (ErrorUtils.class) {
//這裡非同步去請求,然後再確定返回值,獲取併發的時間差
if (isqurst) {
isqurst = false;
return RetrofitUtil.
getInstance()
.create(API.class)
.Login("wangyong", "111111")
.flatMap(loginBean -> {
SPUtils.saveString("token", loginBean.getData().getToken());
isqurst = true;
//這裡建立一個新流去return,保證了先去請求token,之後再去重複訂閱
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});
} else {
return Observable.interval(50, TimeUnit.MILLISECONDS)
.flatMap(it -> Observable.just(isqurst))
.filter(o -> isqurst)
.flatMap(o -> {
Log.e("rrrrrrrr", "eeeeeeeeeeee");
return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
});
}
}
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}).retryWhen(throwableObservable -> {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Throwable throwable) throws Exception {
if (throwable instanceof ApiException && ((ApiException) throwable).getErrorCode() == -999) {
return Observable.just(1);
} else {
//如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
return Observable.error(throwable);
}
}
});
});
}複製程式碼
經過測試,併發問題已經完美解決。不管是請求重新整理token介面還是跳轉登入頁面,只會有一次。
到此關於全域性錯誤處理和token錯誤單獨處理算是完結。而且是插拔式,需要的時候直接新增一行程式碼到請求介面即可,個人感覺比自定義攔截器和動態代理解決方式更加優雅。