一前言
联网框架已经是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() {
@Override
public void subscribe(ObservableEmitter emitter) throws Exception {
emitter.onNext(1);
}
}).subscribe(new Observer() {
@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 ObservableTransformer 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 getProxy(Class 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, ObservableSource>>() {
@Override
public ObservableSource> apply(Observable throwableObservable) {
//这里return决定他是否继续订阅
return throwableObservable.flatMap(new Function>() {
@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 ObservableTransformer specialErrorHandler() {
return upstream ->
upstream .onErrorResumeNext(new Function>() {
@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, ObservableSource>>() {
@Override
public ObservableSource> apply(Observable throwableObservable) throws Exception {
return throwableObservable.flatMap(new Function>() {
@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,而是去跳转登录界面,登录完成之后,继续请求未登录之前的接口。这个需求都是需要上下文对象,很明显第二种实现方式会更加灵活,扩展性更好。
下一篇文章笔者去实现上边的两种需求和解决并发问题。