错误码全局处理(一)

一前言

联网框架已经是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,而是去跳转登录界面,登录完成之后,继续请求未登录之前的接口。这个需求都是需要上下文对象,很明显第二种实现方式会更加灵活,扩展性更好。

下一篇文章笔者去实现上边的两种需求和解决并发问题。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值