5.3 Futuristic Errors
上一节中我们假设 future 结果总是会成功。 现在我们看看如果异步执行过程中抛出了异常会发生什么。 在命令行中开启一个 scala REPL,按照下图中的列表中的命令输入:
Listing 5.8 从 Future 中抛出异常
scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.concurrent._
import ExecutionContext.Implicits.global
val futureFail = Future { throw new Exception("error!")}
futureFail.foreach(value=> println(value))
// Exiting paste mode, now interpreting.
futureFail: scala.concurrent.Future[Nothing] =
  scala.concurrent.impl.Promise$DefaultPromise@193cd8e1
scala>
- 一旦 Future 完成,打印其值
- 因为异常发生,什么也没打印
上述列表中的代码在另一个线程中抛出了异常。 你首先注意到的是,终端上没有堆栈踪迹(stacktrace),如果异常是直接抛出的,你应该会看到。 foreach 块没有执行。 这是因为 future 没有以成功完成。 获取失败值得方法之一是使用 onComplete 方法。 这个方法接受形如 foreach 和 map 的函数作为参数,本例中为函数提供的是 scala.util.Try 值。 Try 的值可以是 Success 或 Failure。 下边的 REPL 展示了如何用它来打印异常。
Listing 5.9 使用 onComplete 来处理 Success 和 Failure
scala> :paste
// Entering paste mode (ctrl-D to finish)
import scala.util._
import scala.util.control.NonFatal
import scala.concurrent._
import ExecutionContext.Implicits.global
val futureFail = Future { throw new Exception("error!")}
futureFail.onComplete {
  case Success(value) => println(value)
  case Failure(NonFatal(e)) => println(e)
}
// Exiting paste mode, now interpreting.
java.lang.Exception: error!
- 引入 Try, Success 和 Failure 语句
- 只捕获非致命的错误是个好习惯
- 打印出成功值
- 打印出非致命的异常
- 打印出了异常
onComplelte 方法使得其能处理成功或失败的结果。 注意在上边的例子中,即使 future 已经完成, onComplete 回调函数也会执行,在上边的例子中,future 很可能已经完成,因为在 future 块中直接抛出了异常。 这堆所有注册在 future 上的函数都成立。
onComplete 方法返回 Unit,所以我们不能串联到下一个函数。 类似地,有 onFailure 方法来匹配失败情况。 onFailure 也返回 Unit,我们不能进一步串联。 下面的列表展示了 onFailure 的使用:
Listing 5.10 使用 onFailure 来匹配所有的非致命异常
futureFail.onFailure {
    case NonFatal(e) => println(e)
}
- 函数失败时被调用
- 匹配所有的非致命异常类型
我们需要在异常发生时还能够继续在 TicketInfo 服务中聚集信息。 TicketInfo 聚集关于活动的信息,如果要求的服务抛出了异常,它应该能够忽略这部分信息。 下图中展示了关于活动的信息在 TicketInfo 类中被聚集起来,作为 TicketInfo 服务流的一部分。
Figure 5.7 在 TicketInfo 类中聚集关于活动的信息
我们把 getEvent 和 getTraffic 方法修改得返回 TicketInfo(在 Future 内部),其可以用来在后边的串联中进一步聚集信息。 TicketInfo 类是一个包含服务结果可选值的简单 case 类。 下边的列表展示了 TicketInfo case 类。 下一节中我们会在这儿类中增加更多信息,像天气预报和其他活动建议。
Listing 5.11 TicketInfo case 类
case class TicketInfo(ticketNr:String,
    event:Option[Event]=None,
    route:Option[Route]=None
)
- 所有关于 ticketNr 的额外的信息都是可选的,默认为空
需要注意的是,使用 futures 时,必须使用不变的数据结构。 否则可能会出现在 futures 之间共享可变状态, futures 可能使用相同的对象。 这里我们是安全的,因为我们使用的是不变的 case 类和 Options。 当服务失败时,串联会继续进行,仍使用聚集到现在的 TicketInfo。 下图展示了应当如何处理失败的 getTraffic 调用。
Figure 5.8 忽略失败的服务应答
recover 方法可以用来达到这一点。 这个方法可以用来定义异常发生时应该返回何值。 下边的片段展示了当 TrafficServiceException 抛出时如果用它出返回输入用的 TicketInfo。
Listing 5.12 使用另外的 Future 结果作为参数调用 recover 来继续
val futureStep1 = getEvent(ticketNr)
val futureStep2 = futureStep1.flatMap { ticketInfo =>
      getTraffic(ticketInfo).recover {
    case e:TrafficServiceException => ticketInfo
  }
}
- getEvent 返回 Future[TicketInfo]
- 使用 flatMap 我们可以直接返回 Future[TicketInfo],而不是来自代码块中的 TicketInfo 值
- getTraffic 返回 Future[TicketInfo].
- 用包含初始 TicketInfo 值的 Future 来恢复
上述的 recover 方法定义了,当 TrafficServiceException 发生时,它应该返回原始的它当做参数接收的 ticketInfo 。 正常情况下, getTraffic 创建一个附加了路线的 TicketInfo 备份。 在上述例子中,我们在 getEvent 返回的 future 上使用 flatMap 而不是 map。 在传给 map 的代码块中,你需要返回 TicketInfo 值,其包含在一个新的 Future。 使用 flatMap 你需要直接返回 Future[TicketInfo]。 因为 getTraffic 已经返回了 Future[TicketInfo], 使用 flatMap 更容易。
类似地,存在 recoverWith 方法,其中代码块需要返回 future[ticketInfo],而不是本例中 recover 分支中的 TicketInfo 值 注意传递给 recover 方法的代码块在错误返回后异步执行。
在上述代码中仍然遗留有一个问题。 如果第一个 getEvent 调用失败会发生什么? flatMap 中的代码块不会被调用, 因为 futureStep1 是个失败了得 Future,所以没有值可以让下一个调用串联。 futureStep2 的值域 futureStep1 完全一样,是个失败的 future 结果。 如果我们想返回值包含 ticketNr 的空 TicketInfo,我们也可以从第一步恢复,如下边的列表所示。
Listing 5.13 如果 getEvent 失败,使用 recover 返回空 TicketInfo
val futureStep1 = getEvent(ticketNr)
val futureStep2 = futureStep1.flatMap { ticketInfo =>
    getTraffic(ticketInfo).recover {
        case e:TrafficServiceException => ticketInfo
    }
    }.recover {
        case NonFatal(e) => TicketInfo(ticketNr)
    }
- 在 getEvent 失败时,返回只包含 ticketNr 的空 TicketInfo
flatMap 调用中的代码不会执行。 flatMap 简单返回失败的 future 结果。 上述列表中,如果 Future 包含非致命的 Throwable,最后的 recover 调用将这个失败了得 Future 变为 Future[TicketInfo]。 既然你已经学会了在 futures 链中如何从错误恢复,我们来看看结合 TicketInfo 服务 futures 的更多方式。