一前言
聯網框架已經是Rxjava和Retrofit的天下。但是錯誤碼的統一封裝目前參差不齊。筆者將通過這篇文章自訴怕坑歷程。在此首先感謝梅老闆的指點。
每個app都有自定義的API錯誤,比如token失效錯誤,引數錯誤等。一般後臺會給我們返回一個錯誤的狀態碼。如下json(為了講述方便,我們規定0表示正確,其餘錯誤碼都是表示不同的錯誤)
{"data":"","error_code":8,"msg":"請重新登入"}複製程式碼
{"data":null,"error_code":8,"msg":"請重新登入"}複製程式碼
.addConverterFactory(GsonConverterFactory.create())//可以新增自定義解析器和預設的解析器
複製程式碼
這個時候你有兩種解決方式:
- 自己自定義解析器,自己拋異常
- 讓你們後臺改成第二種返回的結果
自定義解析器方式
讓你們後臺改成第二種返回的結果
聯網正確,解析正確,只是單純的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
動態代理本質就是動態的去擴充套件方法中的邏輯,而且沒有耦合性。這裡我們要擴充套件的方法是什麼?
擴充套件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,而是去跳轉登入介面,登入完成之後,繼續請求未登入之前的介面。這個需求都是需要上下文物件,很明顯第二種實現方式會更加靈活,擴充套件性更好。
下一篇文章筆者去實現上邊的兩種需求和解決併發問題。