错误码全局处理(二)

一引文

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



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值