一引文
上一篇文章笔者通过自己的怕坑经历阐述了全局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错误单独处理算是完结。而且是插拔式,需要的时候直接添加一行代码到请求接口即可,个人感觉比自定义拦截器和动态代理解决方式更加优雅。