錯誤碼全域性處理(二)

稀飯_發表於2018-09-17

一引文

上一篇文章筆者通過自己的怕坑經歷闡述了全域性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錯誤單獨處理算是完結。而且是插拔式,需要的時候直接新增一行程式碼到請求介面即可,個人感覺比自定義攔截器和動態代理解決方式更加優雅。



相關文章