1. 概述
在 Scala 中,Future
提供了一种声明式的方式来处理并发,隐藏了异步编程的复杂性。我们可以使用 map
对 Future
进行转换,改变其成功完成后的值。但问题是:如果一个 Future
失败了怎么办?我们能否将失败也映射成一个新的值?本文将详细介绍如何实现这一点。
2. 场景示例
先来看一个简单的例子。假设我们要实现一个天气预报服务,给定一个合法的日期,返回当天的天气情况。
首先,我们需要定义一些用于建模天气的类:
sealed trait Weather
case object Sunny extends Weather
case object Cloudy extends Weather
case object Rainy extends Weather
case object Windy extends Weather
case object Snowy extends Weather
case object Foggy extends Weather
✅ 目前为止一切顺利。
接下来编写我们的服务。注意,我们的天气预报服务返回的不是直接的 Weather
,而是 Future[Weather]
。因为实际场景中,天气服务会通过外部 HTTP 接口获取数据。调用远程服务的成本很高,可能需要等待几十甚至几百毫秒的响应时间!
作为谨慎的开发者,我们不希望线程被阻塞等待 HTTP 响应。因此,我们将调用交给 ExecutionContext
处理,并将结果包装进 Future
中。为了简化说明,这里给出一个简化的 HTTP 客户端实现:
import scala.concurrent.ExecutionContext.Implicits.global
class HttpClient {
def get(url: String): Future[String] =
if (url.contains("2020-10-18"))
Future("Sunny")
else if (url.contains("2020-10-19"))
Future("Windy")
else {
Future {
throw new RuntimeException
}
}
}
这个简单的实现可以模拟我们服务的行为。现在是时候实现真正的天气预报服务了。
3. 如何恢复一个失败的 Future
3.1. 使用同步计算恢复
最原始的版本非常简单粗暴,只是直接调用 HttpClient.get
方法:
class WeatherForecastService(val http: HttpClient) {
def forecast(date: String): Future[Weather] =
http.get(s"http://weather.now/rome?when=$date")
}
❌ 问题在于,这种实现把错误处理的责任完全抛给了客户端。而客户端可能只关心天气信息,不想处理异常。
✅ 更好的做法是,在失败时返回上一次获取到的天气值。为此,我们可以在服务中添加一个属性来保存最近一次的天气:
var lastWeatherValue: Weather = Sunny
⚠️ 注意:这里为了演示方便,暂时忽略变量的可变性和并发安全问题。
那么问题来了:如何从一个失败的 Future
中恢复?
幸运的是,从 Scala 2.12 开始,Future
提供了 transform
方法:
def transform[S](f: (Try[T]) ⇒ Try[S]): Future[S]
💡 简单来说,transform
方法会对当前 Future
的结果应用指定函数,并生成一个新的 Future
。
由于输入函数接受的是 Try[T]
类型,我们可以同时处理成功和失败两种状态。而且该函数返回另一个 Try[S]
,意味着我们可以决定是否将失败转换为新的成功值,或者继续保留失败状态。
回到我们的场景,我们可以这样处理失败:
def forecast(date: String): Future[Weather] = {
http.get(s"http://weather.now/rome?when=$date")
.transform {
case Success(result) =>
val retrieved = Weather(result)
lastWeatherValue = retrieved
Try(retrieved)
case Failure(exception) =>
println(s"Something went wrong, ${exception.getMessage}")
Try(lastWeatherValue)
}
}
✅ 这样一来,即使请求失败,也能返回上一次的结果。
3.2. 使用异步计算恢复
如果恢复逻辑本身也需要发起异步请求(比如备用服务),该怎么办?
好消息是,Scala 提供了 transformWith
方法:
def transformWith[S](f: Try[T] => Future[S]): Future[S]
💡 与 transform
类似,transformWith
接受一个返回 Future
的函数,可以用于异步恢复逻辑。
这类似于对 Future
执行 flatMap
操作,同样适用于成功或失败的情况。
在我们的例子中,可以在主服务失败时尝试调用备用接口:
def forecast(date: String, fallbackUrl: String): Future[Weather] =
http.get(s"http://weather.now/rome?when=$date")
.transformWith {
case Success(result) =>
val retrieved = Weather(result)
lastWeatherValue = retrieved
Future(retrieved)
case Failure(exception) =>
println(s"Something went wrong, ${exception.getMessage}")
http.get(fallbackUrl).map(Weather(_))
}
✅ 这样,即便主服务宕机,用户依然可以获得天气信息。
3.3. 在旧版本 Scala 中如何恢复
在 Scala 2.12 之前,transform
的签名略有不同:
def transform[S](s: (T) ⇒ S, f: (Throwable) ⇒ Throwable): Future[S]
⚠️ 这个版本只允许将失败转换为另一个异常,无法将其变为普通值。
所以当时的做法是结合使用 map
和 recover
:
def forecastUsingMapAndRecover(date: String): Future[Weather] =
http.get(s"http://weather.now/rome?when=$date")
.map { result =>
val retrieved = Weather(result)
lastWeatherValue = retrieved
retrieved
}
.recover {
case e: Exception =>
println(s"Something went wrong, ${e.getMessage}")
lastWeatherValue
}
✅ 如果你也需要类似 transformWith
的效果,可以用 flatMap
+ recoverWith
:
def forecastUsingFlatMapAndRecoverWith(date: String, fallbackUrl: String): Future[Weather] =
http.get(s"http://weather.now/rome?when=$date")
.flatMap { result =>
val retrieved = Weather(result)
lastWeatherValue = retrieved
Future(retrieved)
}
.recoverWith {
case e: Exception =>
println(s"Something went wrong, ${e.getMessage}")
http.get(fallbackUrl).map(Weather(_))
}
4. 小结
本文介绍了 Scala 中处理 Future
成功和失败状态的各种方式:
- ✅ Scala 2.12+ 提供了
transform
和transformWith
- ⚠️ 旧版本则依赖
map
/flatMap
+recover
/recoverWith
这些技巧在处理异步操作时非常实用,特别是当你需要优雅降级或重试机制时。
如需查看完整代码,请访问 GitHub。