不要打破链式调用!一个极低成本的RxJava全局Error处理方案

RxJava 给我们的事件驱动型编程带来了新的思路,RxJavaObservable 一下子把我们的维度拓展到了时间和空间两个维度

事件驱动型编程这个词很准确,现在我重新组织我的语言,”不要打破链式调用!“,这句话更应该说,不要破坏RxJava事件驱动型的编程思想。

你到底想说什么?

现在让我们回到文章的标题上,Android开发中,网络请求的错误处理一直是一个无法回避的需求,有了随着RxJava + Retrofit的普及,难免会遇到这个问题:

Android开发中 RxJava+Retrofit 全局网络异常捕获、状态码统一处理

这是我17年年初总结的一篇博客,那时我对于RxJava的理解比较有限,我阅读了网上很多前辈的博客,并总结了文中的这种方案,就是把全局的error处理放在onError()中,并将Subscriber包装成MySubscriber

public abstract class MySubscriber extends Subscriber {
 // …
@Override
public void onError(Throwable e) {
onError(ExceptionHandle.handleException(e)); // ExceptionHandle中就是全局处理的逻辑,详情参考上方文章
}

public abstract void onError(ExceptionHandle.ResponeThrowable responeThrowable);
}

api.requestHttp() //网络请求
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new MySubscriber(context) { // 包装了全局error处理逻辑的MySubscriber
@Override
public void onNext(Model model) { // … }

@Override
public void onError(ExceptionHandle.ResponeThrowable throwable) {
// …
}
});

这种解决方案于我当时看来没有问题,我认为这应该就是 完美的解决方案 了吧。

很快我就意识到了另外一个问题,就是这种方案成功地驱动我写出了 RxJava版本的Callback Hell。

RxJava版本的Callback Hell

我不想你们笑话我的代码,因此我决定先不把它们抛出来,来看一个常见的需求:

请求一个API,如果发生异常,弹出一个Dialog,询问用户是否重试,如果重试,重新请求这个API。

让我们看看可能很多开发者 第一直觉 会写出的代码(为了保证代码不那么啰嗦,这里我使用了Kotlin):

api.requestHttp()
.subscribe(
onNext = {
// …
},
onError = {
AlertDialog.Builder(context) // 弹出一个dialog,提示用户是否重试
.xxxx
.setPositiveButton(“重试”) { _, _ -> // 点击重试按钮,重新请求
api.requestHttp()
.subscribe(
onNext = { … },
onError = { … }
)
}
.setNegativeButton(“取消”) { _, _ -> // 啥都不做 }
.show()
}
)

瞧!我们写出了什么!

现在你也许明白了我当时的处境,onError()onComplete()意味着这次订阅事件的终止,如果全局的异常处理都放在onError()中,接下来如果还有其他的需求(比如网络请求),就意味着你要在这个回调方法中再添加一层回调。

在一边高呼RxJava 链式调用简洁好用避免了CallbackHell 时,我们将 响应式编程 扔到了一旁,然后继续 按照日常的思维 写着 如出一辙的代码

如果你觉得这种操作完全可以接受,我们可以将需求升级一下:

如果发生异常,弹出dialog提示用户重试,这种dialog最多可弹出3次。

好的,如果说,最多重试一次,让代码额外增加了1层回调的嵌套(实际上是2层,Dialog的点击事件本身也是一层回调),那么最多重试3次,就是…4层回调:

api.requestHttp()
.subscribe(
onNext = {
// …
},
onError = {
api.requestHttp()
.subscribe(
onNext = {
// …
},
onError = {
api.requestHttp()
.subscribe(
onNext = { … },
onError = { … } // 还有一层
)
}
)
}
)

你可以说,我把这个请求封装成一个函数,然后每次只调用函数就行了,话虽如此,你依然不能否认这种 CallbackHell 并不优雅。

现在,如果有一种优雅的解决方案,那么这种方案最好有哪些优点?

如有可能,我希望它能做到的是:

1.轻量级

轻量级意味着 较低的依赖成本,如果一个工具库,它又要依赖若干个三方库,首先apk体积的急速膨胀就令人无法接受。

2.灵活

灵活意味着 更低的迁移成本,我不希望,添加 或者 移除 这个工具令我的整个项目发生巨大的改动,甚至是重构。

如有可能,不要在已有的业务逻辑代码上进行修改

3.低学习成本

低的学习成本 可以让开发者更快的上手这个工具。

4.可高度扩展

如有可能,请让这个工具库能够为所欲为

这样看来,上文中通过继承的方式对全局error的处理方案,存在着一定的局限性,抛开令人瞠目结舌的回调地狱之外,不能用lambda表达式 就已经让我难以忍受。

RxWeaver: 一个轻量且灵活的全局Error处理中间件

我花了一些时间开源了这个工具:

RxWeaver: A lightweight and flexible error handler tools for RxJava2.

Weaver 翻译过来叫做 织布鸟,我最初的目的也正是让这个工具能够对逻辑代码正确地组织,达到实现RxJava全局Error处理的需求。

怎么用?可以做到什么程度?

为了代码的足够简洁,我选择使用Kotlin作为示范代码,我保证你可以看懂并理解它们——如果你的项目中适用的开发语言是Java,也请不用担心, RxWeaver 同样提供了Java版本的依赖和示例代码,你可以在这里找到它。

RxWeaver的配置非常简单,你只需要配置好对应的GlobalErrorTransformer类,然后在需要处理error的网络请求代码中,通过compose()操作符,将GlobalErrorTransformer交给RxJava, 请注意,仅仅需要一行代码

private fun requestHttp() {
serviceManager.requestHttp() // 网络请求
.compose(RxUtils.handleGlobalError(this)) // 加上这行代码
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe( // …)
}

RxUtils.handleGlobalError<UserInfo>(this)类似Java中的静态工具方法,它会返回一个对应GlobalErrorTransformer的一个实例——里面存储的是对应的error处理逻辑,这个类并不是 RxWeaver 的一部分,而是根据不同项目的不同业务,自己实现的一个类:

object RxUtils {

fun handleGlobalError(activity: FragmentActivity): GlobalErrorTransformer {
// …
}
}

现在我们需要知道的是,这样一行代码,可以做到什么样的程度

让我们从3个不同梯度的需求看看这个工具的韧性:

1.当接受到某种Error时,Toast对应的信息展示给用户

这是最常见的一种需求,当出现某种特殊异常(本案例以JSONException为例),我们会通过Toast提示这样的消息给用户:

全局异常捕获-Json解析异常!

fun test() {
Observable.error(JSONException(“JSONException”))
.compose(RxUtils.handleGlobalError(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// …
}
}

毫无疑问,当没有加compose(RxUtils.handleGlobalError<UserInfo>(this))这行代码时,这次订阅的结果必然是弹出一个 “onError:xxxx”的 toast。

现在我们加上了compose的这行代码,让我们拭目以待:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

看起来成功了,即使我们在onError()里面针对Exception做出了单独的处理,但是这个JSONException依然被全局捕获了,并弹出了一个额外的toast :“全局异常捕获-Json解析异常!” 。

这似乎是一个很简单的需求,我们提升一点难度:

2.当接收到某种Error时,弹出Dialog

这次需求是:

若接收到一个ConnectException(连接异常),我们让弹出一个dialog,这个dialog只会弹一次,若用户选择重试,重新请求API

又回到了上文中这个可能会引发 Callback Hell 的需求,我们疑问,如何保证 Dialog和重试逻辑正确执行的同时,不打破Observable流的连续性(链式调用)

fun test2() {
Observable.error(ConnectException()) // 这次我们把异常换成了ConnectException
.compose(RxUtils.handleGlobalError(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
// …
}
}

依然是熟悉的代码,这次我们把异常换成了ConnectException,我们直接看结果:

因为我们数据源是一个固定的ConnectException,因此我们无论怎么重试,必然都只会接收到ConnectException,这不重要,你发现没有,即使是一个复杂的需求(弹出dialog,用户选择后,决定是否重新请求这个流),RxWeaver 依然可以胜任。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后一个案例,让我们再来一个更复杂的。

3.当接收到Token失效的Error时,跳转login界面。

详细需求是:

当接收到Token失效的Error时,跳转login界面,用户重新登录成功后,返回初始界面,并重新请求API;如果用户登录失败或取消登录,弹出错误信息。

显然这个逻辑有点复杂了, 对于实现这个需求来讲,似乎不太现实,这次是否会束手无策呢?

fun test3() {
Observable.error(TokenExpiredException())
.compose(RxUtils.handleGlobalError(this))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
subscribe {
// …
}
}

这次我们把异常换成了TokenExpiredException(因为直接实例化一个HttpException过于复杂,所以我们自定义一个异常模拟代替它),我们直接看结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当然,无论怎么重试,数据源始终只会发射TokenExpiredException,但是我们成功实现了这个看似复杂的需求。

4. 我想说明什么?

我认为RxWeaver达到了我心目中的设计要求:

  • 轻量级

你不需要担心 RxWeaver 的体积,它足够的轻量,轻量到所有类加起来只有不到200行代码,同时,除了RxJavaRxAndroid,它 没有任何其它的依赖 ,体积大小只有3kb。

  • 灵活

RxWeaver 的配置不需要 修改 或者 删除 任意一行已经存在的业务代码——它是完全可插拔的。

  • 低学习成本

它的原理也是非常 简单 的,只要熟悉了onErrorResumeNextretryWhendoOnError这几个关键的操作符,你就可以马上上手对应的配置。

  • 高扩展性

可以通过接口实现任意复杂的需求实现。

原理

这似乎本末倒置了,对于一个工具来说,熟练使用API 往往比 阅读源码并了解原理 优先级更高一些。但是我的想法是,如果你先了解了原理,这个工具的使用你会更加得心应手。

RxWeaver的原理复杂吗?

实际上,RxWeaver的源码非常简单,简单到组件内部 没有任何Error处理逻辑,所有的逻辑都交给用户进行配置,它只是一个 中间件

它的原理也是非常 简单 的,只要熟悉了onErrorResumeNextretryWhendoOnError这几个关键的操作符,你就可以马上上手对应的配置。

1.compose操作符

对于全局异常的处理,我只需要在既有代码的 链式调用 加上一行代码,配置一个 GlobalErrorTransformer<T> 交给 compose() 操作符————这个操作符是 RxJava 给我们提供的可以面向 响应式数据类型 (Observable/Flowable/Single等等)进行 AOP 的接口, 可以对响应式数据类型 加工修饰 ,甚至 替换

这意味着,在既有的代码上,使用compose()操作符,我可以将一段特殊处理的逻辑代码插入到这个Observable中,这实在太方便了。

对compose操作符不了解的同学,请参考 【译】避免打断链式结构:使用.compose()操作符 @by小鄧子

compose() 操作符需要我传入一个对应 响应式类型 (Observable/Flowable/Single等等)的Transformer接口,但是问题是不同的 响应式类型 对应不同的 Transformer 接口,不同的于是我们实现了一个通用的 GlobalErrorTransformer<T> 接口以 兼容不同响应式类型的事件流

class GlobalErrorTransformer constructor(
private val globalOnNextRetryInterceptor: (T) -> Observable = { Observable.just(it) },
private val globalOnErrorResume: (Throwable) -> Observable = { Observable.error(it) },
private val retryConfigProvider: (Throwable) -> RetryConfig = { RetryConfig() },
private val globalDoOnErrorConsumer: (Throwable) -> Unit = { },
private val upStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() },
private val downStreamSchedulerProvider: () -> Scheduler = { AndroidSchedulers.mainThread() }
) : ObservableTransformer<T, T>, FlowableTransformer<T, T>, SingleTransformer<T, T>, MaybeTransformer<T, T>, CompletableTransformer {
// …
}

现在我们思考一下,如果我们想把error处理的逻辑放在GlobalErrorTransformer里面,把这个GlobalErrorTransformer交给compose() 操作符,就等于把error处理的逻辑全部 插入 到既有的Observable事件流中了:

fun test() {
observable
.compose(RxUtils.handleGlobalError(this)) // 插入异常处理逻辑
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
subscribe {
// …
}
}

同理,如果某个API不需要追加全局异常处理的逻辑,只需要把这行代码删掉即可,不会影响其他的业务代码。

这是一个不错的思路,接下来,我们需要思考的是,如何将不同的异常处理逻辑加进GlobalErrorTransformer中?

2.简单的全局异常处理:doOnError操作符

这个操作符的作用实在非常明显了,就是当我们接收到某个 Throwable 时,想要做的逻辑:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这实在很适合大部分简单的错误处理需求,就像上文的需求1一样,当我们接收到某种指定的异常,弹出对应的message提示用户,逻辑代码如下:

when (error) {
is JSONException -> {
Toast.makeText(activity, “全局异常捕获-Json解析异常!”, Toast.LENGTH_SHORT).show()
}
else -> {

}
}

这种错误的处理方式, 不会对既有的Observable进行变换 ,也就是说,JSONException 依然会最终传递到subscribe的 onError() 的回调中——你依然需要实现 onError() 的回调,哪怕什么都不做,如有必要,再进行特殊的处理,否则会发生崩溃。

这种方式很简单,但是涉及复杂的需求就无能为力了,这时候我们就需要借助onErrorResumeNext操作符了。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结语

看到这篇文章的人不知道有多少是和我一样的Android程序员。

35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。

我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。

千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

有了学习的想法就赶快行动起来吧,不要被其他的事情牵绊住了前行的脚步。不要等到裁员时才开始担忧,不要等到面试前一晚才开始紧张,不要等到35岁甚至更晚才开始想起来要学习要进阶。

给大家一份系统的Android学习进阶资料,希望这份资料可以给大家提供帮助。
[外链图片转存中…(img-RHgHxf9R-1712341864977)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 26
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要将`funcGetSevrverCsv`函数返回一个Promise对象,你可以使用`Promise`构造函数来包装异步操作。在这种情况下,你可以将`axios.get`和后续处理CSV数据的代码包装在一个新的Promise对象中,并在异步操作完成后调用`resolve`方法来解析Promise。以下是修改后的代码: ```javascript import axios from 'axios'; const d3 = require('d3-dsv'); export function funcGetSevrverCsv() { return new Promise((resolve, reject) => { let path = '/data/csvdoge-usdt.csv'; axios.get(path) .then((resp) => { const newData = d3.csvParse(resp.data); // 在这里处理CSV数据 resolve(newData); // 解析Promise并将处理后的数据作为参数传递给resolve方法 }) .catch((error) => { // 处理错误 console.error(error); reject(error); // 拒绝Promise并将错误信息作为参数传递给reject方法 }); }); } ``` 在修改后的代码中,我们使用`new Promise`创建一个新的Promise对象,并将异步操作的代码放在Promise的构造函数中。在异步操作成功完成后,我们调用`resolve`方法来解析Promise,并将处理后的数据作为参数传递给`resolve`方法。如果发生错误,我们调用`reject`方法来拒绝Promise,并将错误信息作为参数传递给`reject`方法。 现在,当你调用`funcGetSevrverCsv`函数时,它将返回一个Promise对象,你可以使用`.then`和`.catch`方法来处理Promise的解析和拒绝,如下所示: ```javascript funcGetSevrverCsv() .then((data) => { // 处理解析后的CSV数据 console.log(data); }) .catch((error) => { // 处理错误 console.error(error); }); ``` 请注意,由于`axios.get`是一个异步操作,因此你无法直接将其返回值作为Promise对象。相反,你需要在异步操作完成后手动解析Promise并返回处理后的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值