Akka-http routing DSL在Route運算中拋出的異常是由內向外浮出的:當內層Route未能捕獲異常時,外一層Route會接著嘗試捕捉,依次向外擴展。Akka-http提供了ExceptionHandler類來處理Route運算產生的異常: 簡單來說ExceptionHandler類 ...
Akka-http routing DSL在Route運算中拋出的異常是由內向外浮出的:當內層Route未能捕獲異常時,外一層Route會接著嘗試捕捉,依次向外擴展。Akka-http提供了ExceptionHandler類來處理Route運算產生的異常:
trait ExceptionHandler extends ExceptionHandler.PF {
/**
* Creates a new [[ExceptionHandler]] which uses the given one as fallback for this one.
*/
def withFallback(that: ExceptionHandler): ExceptionHandler
/**
* "Seals" this handler by attaching a default handler as fallback if necessary.
*/
def seal(settings: RoutingSettings): ExceptionHandler
}
object ExceptionHandler {
type PF = PartialFunction[Throwable, Route]
private[http] val ErrorMessageTemplate: String = {
"Error during processing of request: '{}'. Completing with {} response. " +
"To change default exception handling behavior, provide a custom ExceptionHandler."
}
implicit def apply(pf: PF): ExceptionHandler = apply(knownToBeSealed = false)(pf)
private def apply(knownToBeSealed: Boolean)(pf: PF): ExceptionHandler =
new ExceptionHandler {
def isDefinedAt(error: Throwable) = pf.isDefinedAt(error)
def apply(error: Throwable) = pf(error)
def withFallback(that: ExceptionHandler): ExceptionHandler =
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = false)(this orElse that) else this
def seal(settings: RoutingSettings): ExceptionHandler =
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = true)(this orElse default(settings)) else this
}
def default(settings: RoutingSettings): ExceptionHandler =
apply(knownToBeSealed = true) {
case IllegalRequestException(info, status) ⇒ ctx ⇒ {
ctx.log.warning("Illegal request: '{}'. Completing with {} response.", info.summary, status)
ctx.complete((status, info.format(settings.verboseErrorMessages)))
}
case NonFatal(e) ⇒ ctx ⇒ {
val message = Option(e.getMessage).getOrElse(s"${e.getClass.getName} (No error message supplied)")
ctx.log.error(e, ErrorMessageTemplate, message, InternalServerError)
ctx.complete(InternalServerError)
}
}
/**
* Creates a sealed ExceptionHandler from the given one. Returns the default handler if the given one
* is `null`.
*/
def seal(handler: ExceptionHandler)(implicit settings: RoutingSettings): ExceptionHandler =
if (handler ne null) handler.seal(settings) else ExceptionHandler.default(settings)
}
簡單來說ExceptionHandler類型就是一種PartialFunction:
trait ExceptionHandler extends PartialFunction[Throwable, Route]
因為ExceptionHandler就是PartialFunction,所以我們可以用case XXException來捕獲需要處理的異常。留下未捕獲的異常向外層Route浮出。當未處理異常到達最外層Route時統一由最頂層的handler處理。與RejectionHandler一樣,最頂層的handler是通過Route.seal設置的:
/**
* "Seals" a route by wrapping it with default exception handling and rejection conversion.
*
* A sealed route has these properties:
* - The result of the route will always be a complete response, i.e. the result of the future is a
* ``Success(RouteResult.Complete(response))``, never a failed future and never a rejected route. These
* will be already be handled using the implicitly given [[RejectionHandler]] and [[ExceptionHandler]] (or
* the default handlers if none are given or can be found implicitly).
* - Consequently, no route alternatives will be tried that were combined with this route
* using the ``~`` on routes or the [[Directive.|]] operator on directives.
*/
def seal(route: Route)(implicit
routingSettings: RoutingSettings,
parserSettings: ParserSettings = null,
rejectionHandler: RejectionHandler = RejectionHandler.default,
exceptionHandler: ExceptionHandler = null): Route = {
import directives.ExecutionDirectives._
// optimized as this is the root handler for all akka-http applications
(handleExceptions(ExceptionHandler.seal(exceptionHandler)) & handleRejections(rejectionHandler.seal))
.tapply(_ ⇒ route) // execute above directives eagerly, avoiding useless laziness of Directive.addByNameNullaryApply
}
上面的exceptionHandler沒有預設值,看起來好像有可能有些異常在整個Route運算里都不會被捕獲。但實際上Akka-http提供了預設的handler ExceptionHandler.default:
/**
* Creates a sealed ExceptionHandler from the given one. Returns the default handler if the given one
* is `null`.
*/
def seal(handler: ExceptionHandler)(implicit settings: RoutingSettings): ExceptionHandler =
if (handler ne null) handler.seal(settings) else ExceptionHandler.default(settings)
通過這個ExceptionHandler.seal函數設置了最頂層的exception handler。
我們可以通過下麵的方法來定製異常處理的方式:
自定義ExceptionHandler,然後:
1、把Exceptionhandler的隱式實例放在頂層Route的可視域內(implicit scope)
2、或者,直接調用handleExceptions,把自定義handler當作參數傳入,把Route結構中間某層及其所有內層包嵌在handleExceptions中,例如:
val route: Route =
get {
pathSingleSlash {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))
} ~
path("ping") {
handleExceptions(customExceptionHandler) {
onSuccess(Future.successful("ok"))
complete("PONG!")
}
} ~
path("crash") {
sys.error("BOOM!")
}
}
第一種辦法是一種頂層對所有未捕獲異常統一處理的方式,第二種辦法可以限制處理區域針對某層以內的Route進行異常捕捉。
下麵是第一種辦法的使用示範:
object ExceptiontionHandlers {
implicit def implicitExceptionHandler: ExceptionHandler =
ExceptionHandler {
case _: ArithmeticException =>
extractUri { uri =>
complete(HttpResponse(InternalServerError, entity = s"$uri: Bad numbers, bad result!!!"))
}
}
def customExceptionHandler: ExceptionHandler =
ExceptionHandler {
case _: RuntimeException =>
extractUri { uri =>
complete(HttpResponse(InternalServerError, entity = s"$uri: Runtime exception!!!"))
}
}
}
第二種方式的使用示範如下:
val route: Route =
get {
pathSingleSlash {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))
} ~
path("ping") {
onSuccess(Future.successful("ok"))
complete("PONG!")
} ~
handleExceptions(customExceptionHandler) {
path("crash") {
sys.error("BOOM!")
}
}
}
下麵是本次討論中的示範源代碼:
import akka.actor._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.stream._
import StatusCodes._
import scala.concurrent._
object ExceptiontionHandlers {
implicit def implicitExceptionHandler: ExceptionHandler =
ExceptionHandler {
case _: ArithmeticException =>
extractUri { uri =>
complete(HttpResponse(InternalServerError, entity = s"$uri: Bad numbers, bad result!!!"))
}
}
def customExceptionHandler: ExceptionHandler =
ExceptionHandler {
case _: RuntimeException =>
extractUri { uri =>
complete(HttpResponse(InternalServerError, entity = s"$uriRuntime exception!!!"))
}
}
}
object ExceptionHandlerDemo extends App {
import ExceptiontionHandlers._
implicit val httpSys = ActorSystem("httpSys")
implicit val httpMat = ActorMaterializer()
implicit val httpEc = httpSys.dispatcher
val (port, host) = (8011,"localhost")
val route: Route =
get {
pathSingleSlash {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`,"<html><body>Hello world!</body></html>"))
} ~
path("ping") {
onSuccess(Future.successful("ok"))
complete("PONG!")
} ~
handleExceptions(customExceptionHandler) {
path("crash") {
sys.error("BOOM!")
}
}
}
val bindingFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route,host,port)
println(s"Server running at $host $port. Press any key to exit ...")
scala.io.StdIn.readLine()
bindingFuture.flatMap(_.unbind())
.onComplete(_ => httpSys.terminate())
}