玩:如何改进错误处理并避免创建不必要的可抛出实例

Play: How to improve error handling and avoid creation of unnecessary throwable instances

创建和抛出异常是一项代价高昂的任务...而且通常在 Web 应用程序中根本不需要抛出异常。如果控制器使用的服务 class 发生错误,则抛出异常是有意义的......但如果控制器已经意识到该问题(例如,未找到用户),则可以 return 描述问题的 JSON。

一个解决方案可能是 Error 案例 class 仅在必要时扩展 Throwable,即当控制器调用的服务失败时:

object MyErrors {

  trait Error { def getMessage: String }
  final case class UserNotFound(userId: Strig) extends Error { def getMessage = s"user $userId not found" }
  final case class UserNotFoundException(userId: String) extends Throwable (s"user $userId not found") with Error
  final case class DuplicateKey(key: Strig) extends Error { def getMessage = s"key $key already exists" }
  final case class DuplicateKeyException(key: String) extends Throwable (s"key $key already exists") with Error
  ...
}

不管 Error 是否是 Throwable,我都可以这样处理可能的错误:

object Users extends Controller {

  ...

  def find(id: String) = Action { request =>
    userService.find(id).map {
      case Some(user) => Ok(success(user.toJson))
      case None =>
        // UserNotFound implements Error but does not inherits from Throwable
        errors.toResult(UserNotFound(id))
    }.recover { case e =>
      // e is thrown by userService.find() and extends Throwable and implements Error (e.g. DuplicateKeyException)
      errors.toResult(e)
    }
  }
}

errors.toResult 将当前异常或错误映射到适当的 HTTP 结果(例如 BadRequestNotFound 等)并将 e 转换为 JSON - 参见 post 了解完整的实现。

现在我的问题是:是否有更简单的方法来完成此任务?如您所见,我必须为单个错误创建两个不同的案例 classes(在示例中,我重复错误消息两次只是为了简单起见)...

两种不同的想法:

如果异常的昂贵之处在于堆栈跟踪,但如果它不是那么昂贵,您会希望使用异常,那么有一个异常混合器将跳过在 scala.util.control.NoStackTrace[ 中生成堆栈跟踪=29=]

如果更多的是关于编写干净的 FP 代码(异常不适合并且您在 return 类型中有所有可能的结果),您可以使用 scala.util.Either(或者为了减少冗长例如来自 scalactic 库的 Or,或来自 ScalaZ 的 \/)。

如果使用 Either,您会看到 Left 失败,Right 成功。然后有方便的方法在OptionEither之间进行转换。

val opt: Option[Int] = None
val either: Either[String, Int] = opt.toRight("Value for none/left")

如果您使用 right 投影,您甚至可以为如果值不是 Right.

会提前退出的推导式​​做准备
val goodOrBad1: Either[String, Int] = Right(5)
val goodOrBad2: Either[String, Int] = Left("Bad")

val result: Either[String, Int] = for {
  good1 <- goodOrBad1.right
  good2 <- goodOrBad2.right
} yield good1 + good2

// fails with first non Right
result == Left("Bad")

让我们假设 userService.findcartService.find returns Option[Something],这意味着你可以这样做:

def showCart(id: String) = Action { request =>
  val userAndCartOrError: Either[Error, (User, Cart)] = for {
    user <- userService.find(id).toRight(UserNotFound(id)).right
    cart <- cartService.findForUser(id).toRight(NoCart(id)).right
  } yield (user, cart)

  userAndCartOrError.fold(
    error => Errors.toResult(error),
    userAndCart => Ok(Json.toJson(userAndCart))
  )
}

当然,对于 futures 它会变得有点混乱,因为你不能将不同的 monad 混合在一起以便理解 (Future and Either)

好吧,它不一定是凌乱的。异步是一种“效果”,可以以一种新的 Monad(想想它是 Monad 的 Monad)可以用于理解的方式堆叠在析取(Either)之上。这实际上丰富了代数而不改变核心行为或以前的行为转换。为了避免展开两次,请使用 Monad 转换器,例如来自 cats、scalaz 或类似库的 EitherT。

在这种情况下,它将是内部包装 Future[Either[A,B]] 的 EitherT[Future, A, B]。看看这个例子:Herding cats

还可以在 this 讨论中找到更多关于堆叠效果的信息。