1.问题引出
CompletableFuture是异步编程的很好工具,那么对于异步编程,最令人担心的现象就是异常被吞掉了。对应的,同步场景,因为天然是同步等结果,无非就是这么几种情况,返回正常结果,抛出异常,等待超时(设置了超时时间),一直等待(没有设置超时时间),但是对于异步场景来说,无论是监听还是会掉,都有可能因为使用方式的错误导致没有会掉或者没有触发监听,造成的结果就是这次的请求的结果凭空丢失了。
举个最简单的例子:
这是一个CompletableFuture最普通的使用方式,异步执行一个任务,如图所示是一个http请求,拿到结果之后执行complete方法,会触发该future的thenRun方法。但是如果这个http请求抛出了异常会怎么样呢?因为没有catch异常并对异常进行相应的处理,所以这个future永远也不会执行,相当于这个http的结果丢了,回调也不会触发了。
那么正确的做法是什么呢?
这样的话在http请求抛出异常的时候,通过completeExceptionally方法触发了future的回调。这就是一个简单的对于异常的处理,防止异常导致无回调的现象发生。但是本篇文章并不是这么简单的就结束了,而是刚刚开始。因为CompletableFuture提供了很强大的功能,封装了很多方法,这些方法如果使用不当,即使你考虑了异常,可能也不会按照你设想的方式去执行,比如:
这里用thenRun替换掉了上面的whenComplete,那么即使http请求抛出了异常,然后也正确的completeExceptionally了,也不会触发thenRun。诸如此类的情况还有很多,下面我们就一一列举说明。
2.举例分析
1.thenRun
上面已经提到了,thenRun方法是无法被异常回调触发的,我们再把图贴一下:
也就是说,即使http请求抛出了异常,并在catch中给future设置了异常,但是给future通过thenRun添加的runnable的action不会被执行。原因是什么呢?这里的completeExceptionally会触发postComplete,进而调用CompletableFuture的uniRun,至于为什么会调用postComplete,又怎么调用到uniRun,请参考之前的文章:java future专题 2-1 CompletableFuture源码探秘-基础用法(1)_普通网友的博客-CSDN博客,不在这里赘述。
我们再来看一下uniRun的源码:
这里a.result就是通过completeExceptionally设置的异常的封装,即AltResult包裹的异常,它不是null,所以会走到第二个if块中。这里r是AltResult,所以走到第一个if块中,不会走else。在这个if块中,直接调用了completeThrowable方法,这个方法只会用UnSafe设置值,不会有其他的附属动作。
也就是说按照上述写法,如果出现了异常,就会导致thenRun中的逻辑无法被执行,同时这个异常被吞掉了。
那么怎么做才能在使用uniRun的同时,在遇到异常情况,还能感知到这个异常呢?我们看一下uniRun后面的逻辑:
uniRun设置完异常值之后,返回了true,所以会走到最下面的d.postFire,所以在d上添加的响应可以被触发,那么这个d是什么呢?它就是在调用thenRun方法添加action时候创建的CompletableFuture,也是该方法的返回值,那么我们需要做的就是对这个d补充一个action:
这里最后一个whenComplete就可以触发。
究其根本,其实是UniWhenCompletion和UniRun这两种不同的UniCompletion的实现导致的,我们再看下UniWhenCompletion的源码:
这里调用了CompletableFuture的uniWhenComplete方法:
这里无论是能够获得正常结果还是抛出了异常,都会调用f.accept方法,也就保证了即使有异常,也不会出现被忽略掉的情况。
2.thenCombine
看完了简单的场景,我们再看一个相对复杂一点的场景,即thenCombine的使用,它的底层源码逻辑参考:java future专题 2-4 CompletableFuture源码探秘-高级用法(1)_普通网友的博客-CSDN博客
我们先来看demo:
有了上面thenRun对于异常处理的经验,我们可以猜到,这里的thenCombine这个action不会被执行,我们看下原因
如图源码所示,这里当future1和future2都结束之后,会走到tryComplete块中,这里面第一个if判断,r是future1 exception,它是被包装成AltResult的异常,所以在第一个if块中直接就break掉,然后到最后返回true,所以这个action的apply方法没有机会被执行。这里面的r和s分别对应a和b的结果,a是future1,b是future2,所以永远都是future1的结果被设置到了当前的CompletableFuture中(这里我们起个名字较exception future,其实它就是调用thenCombine过程中创建的d)。
那么怎么修补这个问题呢?参考上面的做法,我们可以为thenCombine也添加一个whenComplete的action
这样这个whenComplete添加的action就可以被执行了。到这里,细心地小伙伴可能会发现,这个action有两个参数,一个是result,一个是throwable,但是上面的future1和future2都抛出了异常,这个throwable是哪个异常呢?如果是其中一个,那么另一个异常呢?通过代码运行,我们可以看出这个throwable是future1 exception,我们通过源码分析下原因。
上面提到了,对于exception future,永远都是future1的结果被设置到了当前的CompletableFuture中。
紧接着触发whenComplete添加的action,这个action是exception future触发的action。还是继续看uniWhenComplete方法:
这里的r是AltResult,所以调用f.accept的时候会作为参数传入,它就是future1对应的异常。到这里回答完了第一个问题,throwable是future1对应的异常。
然后我们来回答第二个问题,第二个异常哪里去了?我们通过上面biApply方法可以看到,当把r的结果通过completeThrowable设置之后就直接break掉了,s的结果(也就是future2的结果)就被丢掉了。
那么整体看来,我们虽然正确使用了相应的方法,保证异常能够被拿到,但是当两个future都返回异常的时候,其中一个又被丢掉了,所以我们还是需要再考虑一下,做一点改变。
经过分析,我们知道combine.whenComplete的action触发的时候,两个future都已经执行完了,只不过第一个future(r)的异常结果设置完之后直接break掉了,所以这个whenComplete的参数throwable只是r的异常。那么我们可以在whenComplete方法中添加一些代码:
虽然在参数上只有一个throwable,但是在action方法体里面,我们可以通过future的get方法来拿到future1以及future2的异常结果,而且这个get不会阻塞,因为结果已经拿到了。
到此为止,thenCombine方法的双异常场景的处理流程就说完了。
其实这个场景还可以用另一种方式来取代,比如allOf,具体可以参考:java future专题 2-5 CompletableFuture源码探秘-高级用法(2)_普通网友的博客-CSDN博客