玩:如何改进错误处理并避免创建不必要的可抛出实例
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 结果(例如 BadRequest
、NotFound
等)并将 e
转换为 JSON - 参见 post 了解完整的实现。
现在我的问题是:是否有更简单的方法来完成此任务?如您所见,我必须为单个错误创建两个不同的案例 classes(在示例中,我重复错误消息两次只是为了简单起见)...
两种不同的想法:
如果异常的昂贵之处在于堆栈跟踪,但如果它不是那么昂贵,您会希望使用异常,那么有一个异常混合器将跳过在 scala.util.control.NoStackTrace
[ 中生成堆栈跟踪=29=]
如果更多的是关于编写干净的 FP 代码(异常不适合并且您在 return 类型中有所有可能的结果),您可以使用 scala.util.Either
(或者为了减少冗长例如来自 scalactic 库的 Or
,或来自 ScalaZ 的 \/
)。
如果使用 Either
,您会看到 Left
失败,Right
成功。然后有方便的方法在Option
和Either
之间进行转换。
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.find
和 cartService.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 讨论中找到更多关于堆叠效果的信息。
创建和抛出异常是一项代价高昂的任务...而且通常在 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 结果(例如 BadRequest
、NotFound
等)并将 e
转换为 JSON - 参见
现在我的问题是:是否有更简单的方法来完成此任务?如您所见,我必须为单个错误创建两个不同的案例 classes(在示例中,我重复错误消息两次只是为了简单起见)...
两种不同的想法:
如果异常的昂贵之处在于堆栈跟踪,但如果它不是那么昂贵,您会希望使用异常,那么有一个异常混合器将跳过在 scala.util.control.NoStackTrace
[ 中生成堆栈跟踪=29=]
如果更多的是关于编写干净的 FP 代码(异常不适合并且您在 return 类型中有所有可能的结果),您可以使用 scala.util.Either
(或者为了减少冗长例如来自 scalactic 库的 Or
,或来自 ScalaZ 的 \/
)。
如果使用 Either
,您会看到 Left
失败,Right
成功。然后有方便的方法在Option
和Either
之间进行转换。
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.find
和 cartService.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 讨论中找到更多关于堆叠效果的信息。