播放框架:如何实现正确的错误处理 [英] Play Framework: How to implement proper error handling

查看:127
本文介绍了播放框架:如何实现正确的错误处理的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个具有多个模块的Play应用程序,每个模块都有自己的异常集。这里有三个例子:



模块 common

  package services.common 

trait CommonErrors {

final case class NotFound(id:String)extends Exception(sobject $ id没有找到)
final case class InvalidId(id:String)extends Exception(s$ id is a invalid id)
...

//`toJson`只是一个扩展方法,将异常转换为JSON
def toResult(e:Exception):Result = e match {
case NotFound => Results.NotFound(e.toJson)
case InvalidId => Results.BadRequest(e.toJson)
case _ => Results.InternalError(e.toJson)
}
}

模块 auth

  package services.auth 

trait AuthErrors {

final case class UserNotFound(e:NotFound)extends Exception(suser $ {e.id} not found)
final case class UserAlreadyExists(email:String)extends Exception (s用户标识的$电子邮件已经存在)
...

//`toJson`只是一个扩展方法,将异常转换为JSON
def toResult e:异常):Result = e match {
case UserNotFound => Results.NotFound(e.toJson)
case UserAlreadyExists => Results.BadRequest(e.toJson)
case _ => Results.InternalError(e.toJson)
}
}

模块其他

  trait OtherErrors {

final case class AnotherError(s:String)扩展异常(s另一个错误:$ s)
...

//`toJson`只是一个将异常转换为JSON的扩展方法
def toResult(e:Exception):Result = e match {
case AnotherError => Results.BadRequest(e.toJson)
...
case _ =>结果.InternalError(e.toJson)
}
}

看到,每个trait定义了一组异常,并提供了一个方法来将该异常转换为JSON响应,如下所示:

  {
status:404,
code:not_found,
message:user 123456789123456789123456 not found,
request:https:// myhost .com / users / 123456789123456789123456
}

我想要实现的是每个模块定义其异常,重用 common 模块中定义的模块,并根据需要混合异常特征:

 对象用户扩展Controller {

val errors =新的CommonErrors与AuthErrors with OtherErrors {
//这里我必须重写`toResult`编译器快乐
覆盖def toResult(e:Exception)= super.toResult
}

def find(id:String)= Action {request =>
userService.find(id).map {user =>
Ok(success(user.toJson))
} .recover {case e =>
errors.toResult(e)//这返回相应的结果
}
}
}

如果你看看我如何覆盖 toResult ,我总是返回 super.toResult ,它对应于trait OtherErrors ...中包含的实现,并且此实现可能会错过预期在中找到的某些模式, CommonErrors.toResult



肯定我错过了一些...所以问题是:什么是设计模式来解决问题 toResult 的多个实现

解决方案

您可以使用可克服的特质模式。为了简化原因,我将用 .toJson 替换为 .getMessage



定义基本特征:

  trait ErrorsStack {
def toResult(e:Exception):Result = e匹配{
case _ => Results.InternalServerError(e.getMessage)
}
}

和可堆叠特征:

  trait CommonErrors扩展ErrorsStack {
case类NotFound(id:String)extends Exception(sobject $ id没有找到)
case class InvalidId(id:String)extends Exception(s$ id is a invalid id)

override def toResult(e:Exception):Result = e match {
case e:NotFound => Results.NotFound(e.getMessage)
case e:InvalidId => Results.BadRequest(e.getMessage)
case _ => super.toResult(e)
}
}

trait AuthErrors扩展ErrorsStack {
案例类UserNotFound(id:String)extends Exception(suser $ id not发现)
案例类UserAlreadyExists(电子邮件:String)扩展异常(s用户标识的$电子邮件已存在)

覆盖def toResult(e:Exception):Result = e match {
case e:UserNotFound => Results.NotFound(e.getMessage)
case e:UserAlreadyExists => Results.BadRequest(e.getMessage)
case _ => super.toResult(e)
}
}

trait OtherErrors extends ErrorsStack {
case class AnotherError(s:String)extends Exception(s另一个错误:$ s)

override def toResult(e:Exception):Result = e match {
case e:AnotherError => Results.BadRequest(e.getMessage)

case _ => super.toResult(e)
}
}

所以如果我们有一些堆栈

  val errors =具有其他错误的AuthErrors的新CommonErrors 

并定义了一些帮助器

  import java.nio.charset.StandardCharsets。 UTF_8 
import play.api.libs.iteratee.Iteratee
import concurrent.duration._
import scala.concurrent.Await

def getResult(ex:Exception) = {
val res = errors.toResult(ex)
val body = new String(Await.result(res.body.run(Iteratee.consume()),5秒),UTF_8)
(res.header.status,body)
}

以下代码

  import java.security.GeneralSecurityException 

getResult(errors.UserNotFound(Riddle))
getResult(errors.UserAlreadyExists(Weasley))
getResult(errors.NotFound(Gryffindor sword))
getResult(errors.AnotherError(Snape's death))
getResult基因ralSecurityException(掠夺者地图))

将产生合理的输出

  res0:(Int,String)=(404,用户未找到Riddle)
res1:(Int,String)=(400,由Weasley确定的用户已经存在)
res2:(Int,String)=(404,对象Gryffindor sword not found)
res3:(Int,String)=(400,另一个错误:Snape的死亡)
res4 :(Int,String)=(500,掠夺者地图)

我们也可以重构这个代码,拉案例从特征分类,并使函数更易于组合:

  type Resolver = PartialFunction [Exception,Result] 

object ErrorsStack {
val resolver:Resolver = {
case e => Results.InternalServerError(e.getMessage)
}
}

trait ErrorsStack {
def toResult:Resolver = ErrorsStack.resolver
}

对象CommonErrors {
case类NotFound(id:String)extends Exception(sobject $ id not found)
case class InvalidId(id:String)extends Exception(s$ id是一个无效的id)
val resolver:Resolver = {
case e:NotFound => Results.NotFound(e.getMessage)
case e:InvalidId => result.BadRequest(e.getMessage)
}
}

trait CommonErrors扩展了ErrorsStack {
override def toResult = CommonErrors.resolver orElse super.toResult


对象AuthErrors {
case类UserNotFound(id:String)extends异常(suser $ id not found)
案例类UserAlreadyExists(email:String)扩展异常(由$ email确定的用户已经存在)
val resolver:Resolver = {
case e:UserNotFound => Results.NotFound(e.getMessage)
case e:UserAlreadyExists => result.BadRequest(e.getMessage)
}
}

trait AuthErrors扩展ErrorsStack {
override def toResult = AuthErrors.resolver orElse super.toResult


对象OtherErrors {
case class AnotherError(s:String)extends Exception(s另一个错误:$ s)

val resolver:Resolver = {
case e:AnotherError => result.BadRequest(e.getMessage)
}
}

trait其他错误扩展了ErrorsStack {
override def toResult = OtherErrors.resolver orElse super.toResult
}


I have a Play application with several modules, each of which has its own exception set. Here are three examples:

Module common:

package services.common

trait CommonErrors {

  final case class NotFound(id: String) extends Exception(s"object $id not found")
  final case class InvalidId(id: String) extends Exception(s"$id is an invalid id")
  ...

  // `toJson` is just an extension method that converts an exception to JSON
  def toResult(e: Exception): Result = e match {
    case NotFound => Results.NotFound(e.toJson)
    case InvalidId => Results.BadRequest(e.toJson)
    case _ => Results.InternalError(e.toJson)
  }
}

Module auth:

package services.auth

trait AuthErrors {

  final case class UserNotFound(e: NotFound) extends Exception(s"user ${e.id} not found")
  final case class UserAlreadyExists(email: String) extends Exception(s"user identified by $email already exists")
  ...

  // `toJson` is just an extension method that converts an exception to JSON
  def toResult(e: Exception): Result = e match {
    case UserNotFound => Results.NotFound(e.toJson)
    case UserAlreadyExists => Results.BadRequest(e.toJson)
    case _ => Results.InternalError(e.toJson)
  }
}

Module other:

trait OtherErrors {

  final case class AnotherError(s: String) extends Exception(s"another error: $s")
  ...

  // `toJson` is just an extension method that converts an exception to JSON
  def toResult(e: Exception): Result = e match {
    case AnotherError => Results.BadRequest(e.toJson)
    ...
    case _ => Results.InternalError(e.toJson)
  }
}

As you can see, each trait defines a set of exceptions and provides a method to convert that exception to a JSON response like this:

{
  "status": 404,
  "code": "not_found",
  "message": "user 123456789123456789123456 not found",
  "request": "https://myhost.com/users/123456789123456789123456"
}

What I'm trying to achieve is to have each module defining its exceptions, reusing the ones defined in the common module, and mixin the exception traits as needed:

object Users extends Controller {

  val errors = new CommonErrors with AuthErrors with OtherErrors {
    // here I have to override `toResult` to make the compiler happy
    override def toResult(e: Exception) = super.toResult
  }

  def find(id: String) = Action { request =>
    userService.find(id).map { user =>
      Ok(success(user.toJson))
    }.recover { case e =>
      errors.toResult(e) // this returns the appropriate result
    }
  }
}

If you look at how I've overridden toResult, I always return super.toResult, which corresponds to the implementation contained in trait OtherErrors... and this implementation might miss some patterns that are expected to be found in CommonErrors.toResult.

For sure I'm missing something... so the question is: what's the design pattern to fix the issue with multiple implementations of toResult?

解决方案

You could use Stackable Trait pattern. I'll replaced your .toJson with .getMessage for simplification reason:

define base trait:

trait ErrorsStack {
  def toResult(e: Exception): Result = e match {
    case _ => Results.InternalServerError(e.getMessage)
  }
}

and stackable traits:

trait CommonErrors extends ErrorsStack {
  case class NotFound(id: String) extends Exception(s"object $id not found")
  case class InvalidId(id: String) extends Exception(s"$id is an invalid id")

  override def toResult(e: Exception): Result = e match {
    case e: NotFound => Results.NotFound(e.getMessage)
    case e: InvalidId => Results.BadRequest(e.getMessage)
    case _ => super.toResult(e)
  }
}

trait AuthErrors extends ErrorsStack {
  case class UserNotFound(id: String) extends Exception(s"user $id not found")
  case class UserAlreadyExists(email: String) extends Exception(s"user identified by $email already exists")

  override def toResult(e: Exception): Result = e match {
    case e: UserNotFound => Results.NotFound(e.getMessage)
    case e: UserAlreadyExists => Results.BadRequest(e.getMessage)
    case _ => super.toResult(e)
  }
}

trait OtherErrors extends ErrorsStack {    
  case class AnotherError(s: String) extends Exception(s"another error: $s")

  override def toResult(e: Exception): Result = e match {
    case e: AnotherError => Results.BadRequest(e.getMessage)

    case _ => super.toResult(e)
  }
}

so if we have some stack

val errors = new CommonErrors with AuthErrors with OtherErrors

and defined some helper

import java.nio.charset.StandardCharsets.UTF_8
import play.api.libs.iteratee.Iteratee
import concurrent.duration._
import scala.concurrent.Await

def getResult(ex: Exception) = {
  val res = errors.toResult(ex)
  val body = new String(Await.result(res.body.run(Iteratee.consume()), 5 seconds), UTF_8)
  (res.header.status, body)
}

following code

import java.security.GeneralSecurityException

getResult(errors.UserNotFound("Riddle"))
getResult(errors.UserAlreadyExists("Weasley"))
getResult(errors.NotFound("Gryffindor sword"))
getResult(errors.AnotherError("Snape's death"))
getResult(new GeneralSecurityException("Marauders's map"))

will produce reasonable output

res0: (Int, String) = (404,user Riddle not found)
res1: (Int, String) = (400,user identified by Weasley already exists)
res2: (Int, String) = (404,object Gryffindor sword not found)
res3: (Int, String) = (400,another error: Snape's death)
res4: (Int, String) = (500,Marauders's map)

also we can refactor this code, pulling case classed from traits, and make function's more composable:

type Resolver = PartialFunction[Exception, Result]

object ErrorsStack {
  val resolver: Resolver = {
    case e => Results.InternalServerError(e.getMessage)
  }
}

trait ErrorsStack {
  def toResult: Resolver = ErrorsStack.resolver
}

object CommonErrors {
  case class NotFound(id: String) extends Exception(s"object $id not found")
  case class InvalidId(id: String) extends Exception(s"$id is an invalid id")
  val resolver: Resolver = {
    case e: NotFound => Results.NotFound(e.getMessage)
    case e: InvalidId => Results.BadRequest(e.getMessage)
  }
}

trait CommonErrors extends ErrorsStack {
  override def toResult = CommonErrors.resolver orElse super.toResult
}

object AuthErrors {
  case class UserNotFound(id: String) extends Exception(s"user $id not found")
  case class UserAlreadyExists(email: String) extends Exception(s"user identified by $email already exists")
  val resolver: Resolver = {
    case e: UserNotFound => Results.NotFound(e.getMessage)
    case e: UserAlreadyExists => Results.BadRequest(e.getMessage)
  }
}

trait AuthErrors extends ErrorsStack {
  override def toResult = AuthErrors.resolver orElse super.toResult
}

object OtherErrors {
  case class AnotherError(s: String) extends Exception(s"another error: $s")

  val resolver: Resolver = {
    case e: AnotherError => Results.BadRequest(e.getMessage)
  }
}

trait OtherErrors extends ErrorsStack {
  override def toResult = OtherErrors.resolver orElse super.toResult
}

这篇关于播放框架:如何实现正确的错误处理的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆