錯誤碼全域性處理(一)

稀飯_發表於2018-09-13

一前言

聯網框架已經是Rxjava和Retrofit的天下。但是錯誤碼的統一封裝目前參差不齊。筆者將通過這篇文章自訴怕坑歷程。在此首先感謝梅老闆的指點。

二開始爬坑

每個app都有自定義的API錯誤,比如token失效錯誤,引數錯誤等。一般後臺會給我們返回一個錯誤的狀態碼。如下json(為了講述方便,我們規定0表示正確,其餘錯誤碼都是表示不同的錯誤)

{"data":"","error_code":8,"msg":"請重新登入"}複製程式碼
{"data":null,"error_code":8,"msg":"請重新登入"}複製程式碼
這兩個Json對於我們Android開發來說,可是有很大的不一樣。我們知道Retrofit可以新增解析器
.addConverterFactory(GsonConverterFactory.create())//可以新增自定義解析器和預設的解析器
複製程式碼
如果你們後臺返回第一種,那麼只要是error_code錯誤,我們的請求都會報資料解析異常,原因:內建的解析器解析的是物件,結果你們後臺返回的卻是字串。(這種是因為後臺不規範照成的)

這個時候你有兩種解決方式:

  • 自己自定義解析器,自己拋異常
  • 讓你們後臺改成第二種返回的結果

自定義解析器方式

這種方式的原理是:在ResponseBodyConverter 類中獲取到返回的結果是一個字串,我們要在原來的解析之前,先去解析一遍,這次解析只去解析error_code 和msg欄位,只要是error_code不是0,我們就自己丟擲來一個異常。這樣請求層中只要是錯誤,不管是API錯誤,還是聯網錯誤都會走到OnError回撥中,這樣我們就可以統一處理所有的錯誤,Toast告訴使用者是什麼錯誤。自定義解析器的另外一個好處就是當請求到的資料需要解密的時候,自定義解析器簡直的完美應對。由於篇幅限制,自定義解析器自行google。

讓你們後臺改成第二種返回的結果

後臺改完這種結果之後,這時候我們的錯誤會出現在兩個地方:onNext回撥中和onError回撥中。

聯網正確,解析正確,只是單純的API錯誤,當然會走到OnNext中。

聯網不正確,一定會走onError回撥。

這裡我們就要想辦法把OnNext中關於API錯誤的回撥走到onError中,並且能統一封裝起來。這就需要介紹兩個操作符:flatMap +compose去解決這個問題。

那麼怎麼使用呢?我們通過程式碼去講解:

Observable.create(new ObservableOnSubscribe<Integer>() {
    @Override
    public void subscribe(ObservableEmitter<Integer> emitter) throws Exception {
        emitter.onNext(1);
    }
}).subscribe(new Observer<Integer>() {
    @Override
    public void onSubscribe(Disposable d) {

    }

    @Override
    public void onNext(Integer integer) {

    }

    @Override
    public void onError(Throwable e) {

    }

    @Override
    public void onComplete() {

    }
});複製程式碼

這段程式碼執行起來,預設是走onNext回撥,現在我們要讓他走OnError回撥。我們需要去定義一個靜態方法

public static <T> ObservableTransformer<T, T> APIError() {
    return upstream ->
            upstream.flatMap(it -> {
                if (it.equals(1)) {
                    return Observable.error(new RuntimeException("11"));
                } else {
                    return Observable.just(it);
                }
            });

}複製程式碼

然後在上邊程式碼通過compose新增上這個靜態方法:

compose(ErrorUtils.APIError())複製程式碼

我們再去執行:發現走到了onError回撥。我們通過改變流的整體走向,完成了所有的錯誤都會在onError中去處理。

上邊的程式碼邏輯需要根據實際的業務去做處理,其本質不變,這裡只是給讀者提供一個思路。

三自動重新整理token

由於業務需求變化,增加了自動重新整理token,即使token過期,要求去請求token最新的token,之後再用新的token去請求上次因為token過期請求錯誤的介面,並且這一過程對於使用者來說是無感的。

分析需求:任何介面都有可能token過期,這就要求能統一封裝起來。這裡筆者提供兩種思路:

  • 使用動態代理+retryWhen操作符
  • 只使用Rxhava操作符:retryWhen+onErrorResumeNext
使用動態代理+retryWhen操作符

動態代理本質就是動態的去擴充套件方法中的邏輯,而且沒有耦合性。這裡我們要擴充套件的方法是什麼?

擴充套件Retrofit物件Creat的所有方法

T t = mRetrofit.create(tClass);複製程式碼

然後傳遞到動態代理類裡邊,如下:

public <T> T getProxy(Class<T> tClass) {
    T t = mRetrofit.create(tClass);
    return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class<?>[] { tClass }, new ProxyHandler(t));
}複製程式碼

對應的ProxyHandler類是實現InvocationHandler介面的類(這是動態代理的寫法,看不懂就去google一下動態代理入門)

public class ProxyHandler implements InvocationHandler {

    private Object mProxyObject;

    public ProxyHandler(Object proxyObject) {
        mProxyObject = proxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        try {
            return method.invoke(mProxyObject, args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }


}複製程式碼

invoke方法裡邊就是通過反射呼叫原本的方法。我們只要在他之後去寫這些程式碼邏輯即可。

上邊程式碼修改成這樣:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    return Observable.just(1).flatMap(o -> {
        try {
            return (Observable<?>) method.invoke(mProxyObject, args);
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    });
}複製程式碼

接著就是豐富他的邏輯,讓他可以重試,這就需要介紹rxhava中的一個操作符:retryWhen ,當發生錯誤的時候異常就會首先觸發這個方法執行,而它的返回值決定了是否需要繼續重複上次請求。

關於retryWhen這裡需要說明一下:如果返回流傳送onNext事件,則觸發重訂閱。如果不是,那麼就會把這個錯誤傳遞給上層的onError方法

我們只需要在它之前加上我們特殊的邏輯,就可以讓他再次訂閱。

更多關於它的說明請參考

現在就去新增邏輯

public class ProxyHandler implements InvocationHandler {

    private Object mProxyObject;


    public ProxyHandler(Object proxyObject) {
        mProxyObject = proxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        return Observable.just(1).flatMap(o -> {
                return (Observable<?>) method.invoke(mProxyObject, args);
        }).retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
            @Override
            public ObservableSource<?> apply(Observable<Throwable> throwableObservable) {
                //這裡return決定他是否繼續訂閱
                return throwableObservable.flatMap(new Function<Throwable, ObservableSource<?>>() {
                    @Override
                    public ObservableSource<?> apply(Throwable throwable) throws Exception {
                        //判斷是不是token失效,這裡假如token等於8失效
                        if (throwable instanceof ApiException) {
                            if (((ApiException) throwable).getErrorCode() == 8) {
                                //上邊return的是這裡的return,這裡去請求token,如果請求成功就去建立一個可以重複訂閱的
                                // 如果重新整理token的請求也錯誤,他會直接return一個錯誤也就不會發生再次訂閱,錯誤繼續傳遞下去
                                //這裡你可能會問為什麼網路請求不去切換執行緒,你可以列印一下,他本身就是子執行緒去建立的流,所以不用切換執行緒。
                                return RetrofitUtil.
                                        getInstance()
                                        .create(API.class)
                                        .Login("wangyong", "111111")
                                        .flatMap(loginBean -> {
                                            SPUtils.saveString("token", loginBean.getData().getToken());
                                            //這裡建立一個新流去return,保證了先去請求token,之後再去重複訂閱
                                            return Observable.just(1);
                                        });
                            }
                        }
                        //如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
                        return Observable.error(throwable);
                    }
                });


            }
        });

    }

複製程式碼

上邊的註釋解釋的很清楚,只要認真讀,應該都能看明白。細心的朋友可能發現:請求token的介面沒有訂閱者照樣可以發起網路請求,視乎和 Retrofit沒有訂閱者不會發起請求產生衝突,其實,他並不是沒有訂閱者,這個請求的過程是在流轉換過程中發生的,外部請求過程中已經發生了訂閱,所以這裡能發起請求。

最後就是如何使用:

這裡是無感更新token的使用方式:

RetrofitUtil.getInstance().getProxy(API.class)複製程式碼

這裡是不去更新token的使用方式:

RetrofitUtil .getInstance().create(API.class)複製程式碼

這種實現方式會有一個問題,那就是併發請求時候會出現多次請求Token重新整理介面。如果你的重新整理token介面在token有效期內返回還是原來的token,那麼請求併發幾次請求幾次,如果每次請求重新整理token介面後臺都給你一個新的token而不管token是否過期,那麼請求重新整理token的介面的次數會更多。原因如下圖:


錯誤碼全域性處理(一)


關於併發問題給伺服器帶來額外的壓力。我們稍後在談論怎麼解決。我們先去看怎麼通過第二種方式去解決這個動態重新整理token。

只使用Rxhava操作符:retryWhen+onErrorResumeNext

這種方法和開始講解改變流的走向的思路是一樣的。整體程式碼如下:

public static <T> ObservableTransformer<T, T> specialErrorHandler() {

    return upstream ->
            upstream .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 RetrofitUtil.
                                        getInstance()
                                        .create(API.class)
                                        .Login("wangyong", "111111")
                                        .flatMap(loginBean -> {
                                            SPUtils.saveString("token", loginBean.getData().getToken());
                                 
                                            //這裡建立一個新流去return,保證了先去請求token,之後再去重複訂閱
                                            return Observable.error(new ApiException(-999, "這表示特殊錯誤,表示要重複去請求"));
                                        });
                            } else {
                                //如果不是token錯誤,會建立一個新的流,把錯誤傳遞下去
                                return Observable.error(throwable);
                            }
                        }
                    })
                    .retryWhen(new Function<Observable<Throwable>, ObservableSource<?>>() {
                        @Override
                        public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception {

                            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);
                                    }

                                }
                            });
                        }
                    });複製程式碼

需要解釋的是onErrorResumeNext,他會在發生錯誤的第一時間拿到錯誤型別,緊接著會把錯誤型別再次傳遞給retryWhen,我們可以在retryWhen裡邊通過不同的錯誤,去處理到底是重複請求還是直接把錯誤扔出去。

當然這種實現方式也會帶來併發請求多次重新整理token的問題,我們先放一放這個問題。我們先來對比一下這兩種實現方式的靈活度。

假如需求再次變化要求不去自動重新整理token,而是去跳轉登入介面,登入完成之後,繼續請求未登入之前的介面。這個需求都是需要上下文物件,很明顯第二種實現方式會更加靈活,擴充套件性更好。

下一篇文章筆者去實現上邊的兩種需求和解決併發問題。



相關文章