提出问题
最近做项目中有个小需求,简单说下,有A,B两个接口需要同时请求,一般情况下B接口返回数据会比A接口快,但是我们需要优先拿取A的数据,只有当A接口没有数据返回或者异常的情况下,才会拿取B接口的数据;
- 首先考虑直接用Rxjava中merge或者zip操作符进行并发执行,但是返回的一直都是时间较快的B接口数据,所以不太满足需求,需要加些条件操作符,那么我们需要使用怎么样的逻辑呢,值得商榷下
- 目前要满足两个条件:1.两个接口同时执行,优先A接口数据 2.当A接口没有返回时才返回B接口
这里就需要涉及到Rxjava相关的操作符来实现逻辑,感兴趣的小伙伴可以提供下好的思路,最后我会给出大佬教我的思路来参考,在此之前,我们先来回顾学习下Rxjava中的操作符和常见的Subject,只有知道它们是干什么的才能知道如何去合理的运用它们,工欲善其事必先利其器;
Rxjava操作符
对于Rxjava想必大家都不陌生吧,它提供了丰富的操作符,几乎能胜任所有的功能需求,但是对于操作符的应用场景是千变万化的,如何在不同场景下使用合适的操作符是大家掌握Rxjava操作符使用的基本条件
话不多说,我们根据Rxjava操作符的官方文档,来看看有那些以及它们分别有什么作用呢,贴上官网地址 -->reactivex.io/documentati…
结合文档,我们可以看到每个操作符都有详细的介绍使用,并且提供了鱼骨流程图方便我们更直观的理解每一个操作符的作用。
大致有如下分类
| 分类 | 作用 |
|---|---|
| 创建操作符(Creating Observables) | 创建(被观察者)Observable对象&发送事件 |
| 转变操作符(Transforming Observables) | 转变(被观察者)Observable发送的事件 |
| 过滤操作符(Filtering Observables) | 过滤(被观察者)Observable发送的事件,以及筛选(观察者)Observer接收的事件 |
| 组合操作符(Combining Observables) | 组合多个被观察者(Observable)&合并需要发送的事件 |
| 一些功能性操作符(Error Handling Operators,Connectable Observable Operators) | 对被观察者(Observable)发送事件的时候进行些处理(如错误处理,线程调度等等) |
| 布尔操作符(Conditional and Boolean Operators) | 设置条件,判断被观察者(Observable)发送的事件是否符合条件 |
这些日常开发中常用的一些操作符,类似just,defaultIfEmpty,takeUntil等等,这里我就不多说了,相信网上已经有非常多的详细讲解了,今天主要是通过这些操作符来解决我们上面提到的问题,在这之前,我们继续看下Rxjava中常见的Subject
Rxjava常见的Subject
现在我们来回顾下Rxjava中的Subject,那么Rxjava中的Subject是什么呢? 从官方解释来看,简单来说,它就是使用序列来组成异步的、基于事件的程序的库。
既然是基于事件的,又是观察者模式的一种,那么EventBus的功能它都能够W事件咯,可以看下下图关于它的官方介绍:
可以看到在它的子类有AsyncSubject,BehaviorSubject,PublishSubejct,ReplaySubject,dUnicastSubject, 它们分别有什么作用呢? 下面来说说我们常用的几种Subject(作为观察者亦或者是被观察者)
PublishSubject
publishSubject作为最常用的Subject, 大致作用就是它会发送所有数据给订阅者,一旦某个观察者者订阅了该Subject,如下图所示
可以看到,这个订阅者只能接收订阅之后收到的所有数据,订阅之前的数据就无法收到了,看下下面简单的例子
val publishSubject: PublishSubject<String> = PublishSubject.create()
publishSubject.subscribe()
publishSubject.onNext("one")
publishSubject.onNext("two")
//这时候观察者可以接收到来自PublishObject发射的one,two的事件
publishSubject.subscribe()
publishSubject.onNext("one")
publishSubject.onComplete()
//这时候观察者仅仅可以接收到来自Publish发射的one,onComplete()事件
这也恰恰说明了,订阅者只会接收到订阅之后来自PublishSubject的数据,那么如果我想有没有Subject可以接收订阅之前的数据呢,答案毋庸置疑,有且不止一个,但是最常见运用最多的还是BehaviorSubject
BehaviorSubject
下面我们来简单了解下BehaviorSubject,官方给出的描述大致意思是它会发送订阅最近的上一个事件,如果没有的话,会直接使用给出的默认值,如下图所示
它的Observer接收的是BehaviorSubject被订阅前发送的最后一个数据,看下下面的例子方便理解
fun testBehaviorObject() {
val behaviorSubject1 = BehaviorSubject.createDefault("default")
behaviorSubject1.subscribe()
behaviorSubject1.onNext("one")
behaviorSubject1.onNext("two")
behaviorSubject1.onNext("three")
//这时候会接收被订阅之前的事件,但是还是接收之后发送的onNext事件,不需要我们手动调用onComplete()
val behaviorSubject2 = BehaviorSubject.createDefault("default")
behaviorSubject2.onNext("zeus");
behaviorSubject2.onNext("one");
behaviorSubject2.subscribe();
behaviorSubject2.onNext("two");
behaviorSubject2.onNext("three");
/**
* 由于behaviorSubject定义就是可以接收离订阅最近的一个数据,并且之后还会继续接收数据,所以这时候我们接收
* 的事件就有one,two,three,就没有zeus这个事件,因为它不是离订阅最近的一个数据
*/
}
AsyncSubject
对于AsyncSubject来说,它的作用其实和BehaviorSubject类似,都是接收最近的一个事件数据,但是不同的是,无论有多少事件,它始终只能接收到最后一个事件数据,且必须要我们手动调用onComplete(),否则是无法接收到任何数据;另外如果在发送过程中遇到错误,则观察者仅仅会接收到错误信息,可以看下官方的示例图
可以看到它的Observer会接收到onCompleted()前发送的最后一个数据,之后不会再接收数据,看下下面的例子方便理解
fun testAsyncSubject() {
val asyncSubject1 = AsyncSubject.create<String>()
asyncSubject1.subscribe()
asyncSubject1.onNext("1")
asyncSubject1.onNext("2")
asyncSubject1.onNext("3")
//由于没有调用onComplete方法,所以此时观察者不会接收到事件,如果有异常,则会接收错误信息
val asyncSubject2 = AsyncSubject.create<String>()
asyncSubject2.subscribe()
asyncSubject2.onNext("1")
asyncSubject2.onNext("2")
asyncSubject2.onNext("3")
asyncSubject2.onComplete()
//因为调用了onComplete方法,此时观察者接收到的事件是3,它是离Complete最近的一个数据
}
ReplaySubject
根据官方解释,ReplaySubject会发射所有事件数据给观察者,不管它什么时候订阅的,它都会缓存所有的发射数据,如下图所示
话不多说,我们举个例子来看下
fun testReplaySubject() {
val replaySubject = ReplaySubject.create<String>()
replaySubject.onNext("one")
replaySubject.onNext("two")
replaySubject.onNext("three")
replaySubject.onComplete()
replaySubject.subscribe()
//在这之前的所有发射的数据都能接收到,包括one,two,three,以及onComplete,不管它什么时候订阅
}
回到最初的问题
经过以上Rxjava的操作符和Subject的学习了解,我们回到一开始提出的问题,我们需要满足两个条件
- 两个接口同时执行,优先A接口数据
- 要A接口没有数据返回时,考虑失败后抛出异常,然后返回B接口的数据,亦或者两者都失败了抛出异常
-
首先要保证两个接口同时进行请求,这样可用到的操作符就有merge和zip,但是merge操作符虽然是并行,但是是按照时间顺序来进行发送,所以优先都是请求时间短的接口拿到数据,这里的场景暂时不适合,所以我们使用zip操作符,对结果进行合并进行发送,那么我们可以对两个接口的数据源进行分开判断,通过PublishSubject订阅发送标志位Boolean值,它会接收所有在订阅之后发送的数据,在a接口的被观察者中只有它在doOnNext对该subject发送true,这时候在b接口的被观察者中设置takeUntil操作符,它的值就是前面设置的PublishSubject,这样的话只要a接口返回数据了,b接口就会停止发送数据;综上,我们已经满足了第一个条件了
-
接着就要考虑下如果A接口失败了,这时候是没有数据返回的,由于是zip操作符,所以我们自定义数据源item,将异常都记录起来,在a接口的被观察中的OnErrorReturn操作符中发送这个数据源item,(这个拦截是不会触发onError发送给观察者的异常,它会发送出一个正常的item),如下
data class SourceResult(val result: String?, val exception: Throwable? = null)然后在b接口被观察者中同样也是使用OnErrorReturn,但值得注意的是当a接口拿到数据了,此时b接口就不发送事件了,我们也需要它发送一个默认事件(自定义异常),不发送任何有效事件doOnNext,仅仅发送Complete事件的前提上,这里就用到了defaultIfEmpty操作符,它解决了这个问题,这样我们就满足了第二个条件,至此,这个小问题就得以解决了,可以看下下面的实例代码,简单梳理了下
private val publishSubject = PublishSubject.create<Boolean>() fun getResult(imageUrl: String): Observable<String> { return Observable.just(imageUrl) .flatMap { val aSource = getAResult(imageUrl) //这时候a接口如果有数据返回的话,将publishSubject发送true数据 .doOnNext { publishSubject.onNext(true) } .map { SourceResult(imageUrl, null) } //拦截对应的error进行返回 .onErrorReturn { SourceResult(null, it) } val bSource = getBResult(imageUrl) .map { SourceResult(imageUrl, null) } .onErrorReturn { SourceResult(null, it) } //如果该条件为true的话,就停止发送数据,意味着A接口有数据了 .takeUntil(publishSubject) .defaultIfEmpty(SourceResult(null, RuntimeException())) //zip进行合并 val result = Observable.zip(aSource, bSource) { a, b -> ZipResult(a, b) } return@flatMap result.map { val aResult = it.aSource?.result val bResult = it.bSource?.result when { aResult != null -> { aResult } bResult != null -> { bResult } else -> { //抛出对应的异常,这里我简单抛出b接口的异常 it.bSource?.exception?.let { throw it } } } } } } fun getAResult(imageUrl: String): Observable<String> { return Observable.just(imageUrl) .map { //.... return@map it } } fun getBResult(imageUrl: String): Observable<String> { return Observable.just(imageUrl) .map { return@map it } } } data class SourceResult(val result: String?, val exception: Throwable? = null) data class ZipResult(val aSource: SourceResult?, val bSource: SourceResult? = null)至此,这个小问题就得到了解决,对于Rxjava的操作符和Subject的运用也更加得心应手了