—— CompletableFuture 如何处理异步结果与异常)
上一章介绍了CompletableFuture如何编排多个异步任务。任务正常完成时结果会沿着任务链继续向后传递但真实程序中数据库查询可能失败远程调用可能超时任务代码也可能抛出运行时异常。这一章继续沿用上一章的任务链视角只讨论一个问题异步阶段失败以后异常如何保存、传播、恢复以及调用线程最终如何感知这个失败。一、异步任务中的异常为什么不会直接抛给调用线程先看一个异步任务CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{thrownewIllegalStateException(查询失败);});System.out.println(调用线程继续执行);调用supplyAsync()的线程和执行 Lambda 的工作线程通常不是同一个线程。异常只能沿当前线程的调用栈向上传播工作线程中的异常无法直接跳到主线程的调用栈中。因此CompletableFuture必须先把任务的完成状态保存到堆对象中。正常完成时保存正常结果异常完成时保存异常对象。调用线程之后只能通过异常处理阶段、get()或join()再去感知这次失败。这和前文的FutureTask思路一致跨线程不能靠方法返回值和异常直接传递只能靠共享的结果对象保存完成状态。二、Supplier 为什么不能直接抛出受检异常CompletableFuture.supplyAsync()接收的是SupplierT不是CallableV。函数式接口方法声明能否直接声明受检异常SupplierTT get()否CallableVV call() throws Exception是Supplier.get()没有声明throws所以 Lambda 在实现Supplier.get()时不能直接把受检异常向外抛出。下面的代码无法通过编译CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{Thread.sleep(1000);return完成;});问题不在于CompletableFuture不能保存异常而在于这个 Lambda 正在实现一个不允许抛出受检异常的方法。常见做法是在 Lambda 内部捕获受检异常再包装成运行时异常继续抛出CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{try{Thread.sleep(1000);return完成;}catch(InterruptedExceptione){Thread.currentThread().interrupt();thrownewRuntimeException(e);}});这里的try-catch不是把失败吞掉而是把不能直接抛出的受检异常转换成可以继续传播的运行时异常。捕获InterruptedException后重新调用Thread.currentThread().interrupt()是为了恢复中断标记让后续代码仍然知道当前线程曾经收到过中断请求。如果捕获异常后直接返回默认值CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{try{Thread.sleep(1000);return真正完成;}catch(InterruptedExceptione){return默认结果;}});那么从CompletableFuture的角度看当前阶段最终正常返回了默认结果所以它会被认为正常完成。也就是说异常是否继续进入任务链取决于 Lambda 最终是正常返回还是继续抛出异常。三、异常如何沿任务链传播普通后续阶段只会在前一步正常完成时执行。例如CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{thrownewIllegalStateException(查询失败);}).thenApply(String::toUpperCase);第一阶段异常完成后thenApply()没有正常结果可用因此不会执行。异常会继续向后传播直到遇到能够处理异常的阶段或者最终被get()/join()感知。可以把任务链理解成两条路径不同方法对前一步完成状态的反应不同方法前一步正常完成前一步异常完成thenApply()执行跳过thenAccept()执行跳过thenRun()执行跳过exceptionally()跳过执行handle()执行执行whenComplete()执行执行所以分析异常传播时先不要急着看方法名而要先判断前一步是正常完成还是异常完成当前阶段是否会执行当前阶段执行后异常是被恢复成正常结果还是继续向后传播四、exceptionally只处理失败分支如果任务失败后希望返回一个备用值继续任务链可以使用exceptionally()CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{thrownewIllegalStateException(查询失败);}).exceptionally(ex-默认结果);当原任务异常完成时exceptionally()接收异常并返回默认结果。从这个阶段开始任务链重新变成正常完成Stringresultfuture.join();// 默认结果如果前一步正常完成exceptionally()不会执行正常结果会直接向后传递CompletableFutureStringfutureCompletableFuture.supplyAsync(()-真实结果).exceptionally(ex-默认结果);最终结果仍然是真实结果。因此exceptionally()适合“成功时保留原结果失败时返回备用值”的场景。例如优惠券查询失败时按“无优惠券”处理CompletableFutureCouponcouponFuturequeryCouponAsync().exceptionally(ex-Coupon.empty());需要注意备用值必须与原结果类型兼容。原任务是CompletableFutureCoupon异常处理阶段也必须返回Coupon。五、handle成功和失败都进行转换有时不仅失败时需要处理成功时也需要转换结果。例如成功时把结果转成大写失败时返回默认值可以使用handle()CompletableFutureStringfutureCompletableFuture.supplyAsync(()-queryData()).handle((result,ex)-{if(ex!null){returnDEFAULT;}returnresult.toUpperCase();});handle()无论前一步正常还是异常都会执行。前一步状态resultex正常完成正常结果null异常完成通常为null异常对象handle()不只是恢复异常也可以转换正常结果甚至改变结果类型CompletableFutureIntegerfutureCompletableFuture.supplyAsync(()-123).handle((result,ex)-{if(ex!null){return0;}returnInteger.parseInt(result);});原来的结果类型是String经过handle()后变成Integer。与exceptionally()相比handle()更适合“成功和失败都要统一映射成另一个结果”的场景。如果只是失败时返回备用值exceptionally()通常更直接。六、whenComplete观察结果和异常但不负责恢复whenComplete()也会在成功和失败时都执行但它主要用于观察结果和异常例如记录日志、统计耗时、释放资源CompletableFutureStringfutureCompletableFuture.supplyAsync(()-queryData()).whenComplete((result,ex)-{if(ex!null){System.out.println(执行失败ex);}else{System.out.println(执行成功result);}});whenComplete()接收的回调类似BiConsumer回调本身没有返回值。但whenComplete()方法仍然返回CompletableFutureT所以后续任务可以继续编排。关键点是whenComplete()默认不会把异常恢复成正常结果。下面的代码虽然记录了异常但异常仍然会继续传播CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{thrownewIllegalStateException(查询失败);}).whenComplete((result,ex)-{System.out.println(记录异常ex);});future.join();// 仍然抛出异常如果想先记录异常再返回默认值可以继续接exceptionally()CompletableFutureStringfuturequeryAsync().whenComplete((result,ex)-{if(ex!null){System.out.println(记录异常ex);}}).exceptionally(ex-默认结果);真正把异常恢复成正常结果的是exceptionally()不是whenComplete()。还要注意如果前一步正常完成但whenComplete()自己抛出了异常那么它返回的新阶段会异常完成。因此“whenComplete()不改变原结果”有一个前提whenComplete()自己正常执行结束。七、三种异常处理方法如何选择exceptionally()、handle()和whenComplete()的区别可以放在同一张表里方法正常时执行异常时执行能否返回新结果典型用途exceptionally()否是是失败时返回备用值handle()是是是成功和失败统一转换whenComplete()是是否日志、统计、资源释放选择时可以按这个顺序判断只在失败时恢复用exceptionally()成功和失败都要转换用handle()只想观察结果或异常不想改变结果用whenComplete()。还要区分三处不同的异常处理。第一处是任务定义处CompletableFutureStringfutureCompletableFuture.supplyAsync(()-{try{returnqueryData();}catch(IOExceptione){thrownewRuntimeException(e);}});这里处理的是Supplier.get()不能直接抛出受检异常。第二处是任务链运行过程中future.whenComplete((result,ex)-log(result,ex)).exceptionally(ex-默认结果);这里处理的是已经被CompletableFuture保存下来的异常后续应该如何观察、转换或恢复。第三处是获取最终结果时future.get();future.join();这里处理的是整条任务链最终是正常完成还是异常完成。不要把这三处混为一谈。Lambda 内部的try-catch解决的是函数式接口签名限制exceptionally()/handle()/whenComplete()处理的是异步阶段保存的异常get()/join()处理的是最终消费结果时看到的完成状态。八、get() 与 join() 有什么区别get()和join()都会等待任务完成并取得结果。正常完成时两者得到的结果相同主要区别在于异常处理方式。get()来自Future接口方法声明中包含受检异常Vget()throwsInterruptedException,ExecutionException;调用时必须处理等待线程被中断和任务执行失败两类情况try{Stringresultfuture.get();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}catch(ExecutionExceptione){Throwablecausee.getCause();}InterruptedException表示等待结果的线程被中断ExecutionException表示异步任务执行失败。原始异常保存在ExecutionException.getCause()中。join()是CompletableFuture自己提供的方法Stringresultfuture.join();它不声明InterruptedException和ExecutionException因此编译器不会强制调用者编写try-catch。如果任务异常完成join()通常抛出CompletionException原始异常同样可以通过getCause()取得。方法是否声明受检异常任务失败时的包装异常等待线程中断get()是ExecutionException抛出InterruptedExceptionjoin()否CompletionException不以InterruptedException形式返回join()不强制处理受检异常不代表异常消失了只是异常通过运行时异常继续传播。任务链内部应尽量继续编排不要每一步都调用join()或get()。更合理的方式是先描述完整任务链在真正需要最终结果的边界处再调用get()或join()。九、超时不等于任务停止普通get()可能无限等待。带超时的get()可以让调用线程最多等待一段时间Stringresultfuture.get(1,TimeUnit.SECONDS);超过一秒后等待线程会抛出TimeoutException但工作线程中的任务可能仍然继续执行。也就是说等待超时和任务停止是两件不同的事情。CompletableFuture还提供了和阶段完成状态相关的超时方法。orTimeout()表示规定时间内没有完成时让这个CompletableFuture异常完成CompletableFutureStringfuturequeryAsync().orTimeout(1,TimeUnit.SECONDS);之后可以继续用异常处理阶段恢复CompletableFutureStringfuturequeryAsync().orTimeout(1,TimeUnit.SECONDS).exceptionally(ex-默认结果);completeOnTimeout()表示超时后使用默认值正常完成CompletableFutureStringfuturequeryAsync().completeOnTimeout(默认结果,1,TimeUnit.SECONDS);三者的区别是方法超时后的表现get(timeout)等待线程抛出TimeoutExceptionorTimeout()CompletableFuture异常完成completeOnTimeout()使用默认值正常完成这些方法都不保证底层任务已经真正停止。它们改变的是等待线程的状态或者CompletableFuture本身的完成状态。十、cancel(true) 也只是协作式取消取消任务可以调用future.cancel(true);但cancel(true)只能尝试取消任务不能强制杀死线程。任务能否停止取决于任务是否已经开始执行、任务是否检查中断状态、阻塞操作是否支持中断、任务代码是否正确响应中断以及底层操作是否支持取消。如果任务主动检查中断状态CompletableFutureVoidfutureCompletableFuture.runAsync(()-{while(!Thread.currentThread().isInterrupted()){doWork();}});它更有机会配合取消请求及时结束。如果任务完全不检查中断CompletableFutureVoidfutureCompletableFuture.runAsync(()-{while(true){doWork();}});取消请求可能无法让它及时停止。因此取消是一种协作机制不是强制终止机制。这一点和前文FutureTask.cancel(true)的语义一致。十一、allOf 和 anyOf 遇到异常时会怎样allOf()等待所有任务完成。如果其中至少一个任务异常完成组合后的CompletableFutureVoid也会异常完成。各个任务的结果和异常仍然保存在原来的CompletableFuture中。allOf()本身只表达“这一批任务都完成了”不负责收集每个业务结果。anyOf()在任意一个任务最先完成时结束这里的“完成”包括正常完成和异常完成。所以anyOf()等待的是最快完成不是最快成功。它不会自动忽略失败任务也不会在一个任务完成后自动取消其他仍在执行的任务。十二、常见错误第一捕获异常后直接返回nullcatch(IOExceptione){returnnull;}这会让任务被认为正常完成同时丢失失败原因。除非null是明确设计的业务结果否则不应这样处理。第二捕获InterruptedException后不恢复中断标记。通常应先调用Thread.currentThread().interrupt()再决定继续抛出还是返回备用结果。第三认为whenComplete()已经处理异常。它通常只观察异常不会自动恢复异常状态。第四所有异常场景都使用handle()。如果只是失败时返回备用值exceptionally()更直接。第五在任务链每一步调用join()。这会破坏异步编排让调用线程重新承担等待和推动任务的职责。第六认为超时等于取消。超时通常只改变等待状态或CompletableFuture的完成状态不保证底层任务停止。第七认为cancel(true)一定能停止任务。取消能否成功取决于任务是否配合响应中断。本章总结CompletableFuture的异常处理可以从“跨线程异常不能直接传播”开始理解工作线程中的异常不会跳到调用线程栈中而是被保存到CompletableFuture的完成状态里。后续普通阶段需要正常结果所以遇到异常会被跳过异常会继续向后传播直到被异常处理阶段观察、转换或恢复或者最终在get()/join()处暴露出来。exceptionally()、handle()和whenComplete()解决的是任务链中的异常分支exceptionally()只在失败时执行并可返回备用值handle()成功和失败都会执行并可统一转换结果whenComplete()适合日志、统计和资源释放默认不恢复异常。它们和 Lambda 内部捕获受检异常不是同一层问题也和最终调用get()/join()取结果不是同一层问题。超时和取消也要分开看。get(timeout)只是让等待线程不再无限等待orTimeout()和completeOnTimeout()改变的是CompletableFuture的完成方式但它们都不保证底层任务停止。cancel(true)也只是发出中断请求任务是否结束取决于代码是否配合响应。只有把“异常保存、异常恢复、最终取结果、超时取消”这几条线分清才能正确判断一条CompletableFuture任务链最终会正常完成、异常完成还是被取消。