Akka HTTP Routing DSL

Route 路由

type Route = RequestContext => Future[RouteResult]

Akka HTTP 里路由是類型 Route 只是一個類型別名,它其實是一個函數 RequestContext => Future[RouteResult],它接受一個 RequestContext 參數,並返回 Future[RouteResult]RequestContext保存了每次HTTP請求的上下文,包括HttpRequestunmatchedPathsettings等請求資源,還有4個函數來響應數據給客戶端:html

  • def complete(obj: ToResponseMarshallable): Future[RouteResult]:請求正常完成時調用,返回數據給前端。經過 Marshal 的方式將用戶響應的數據類型轉換成 HttpResponse,再賦值給RouteResult.Complete前端

  • def reject(rejections: Rejection*): Future[RouteResult]:請求不能被處理時調用,如:路徑不存、HTTP方法不支持、參數不對、Content-Type不匹配等。也能夠自定義Rejection類型。java

  • def redirect(uri: Uri, redirectionType: Redirection): Future[RouteResult]:用指定的url地址和給定的HTTP重定向響應狀態告知客戶端須要重定向的地址和方式。redirect其實是對complete的封裝,能夠經過向complete函數傳入指定的HttpResponse實例實現:git

    complete(HttpResponse(
      status = redirectionType,
      headers = headers.Location(uri) :: Nil,
      entity = redirectionType.htmlTemplate match {
        case ""       => HttpEntity.Empty
        case template => HttpEntity(ContentTypes.`text/html(UTF-8)`, template format uri)
      }))
  • def fail(error: Throwable): Future[RouteResult]:將給定異常實例氣泡方式向上傳遞,將由最近的handleExceptions指令和ExceptionHandler句柄處理該異常(若異常類型是RejectionError,將會被包裝成Rejection來執行)。github

RequestContext

RequestContext包裝了HTTP請求的實例HttpRequest和運行時須要的一些上下文信息,如:ExcutionContextMaterializerLoggingAdapterRoutingSettings等,還有unmatchedPath,該值描述了請求UIR還未被匹配的路徑。web

unmatchedPathapi

若請求URI地址爲:/api/user/page,對於以下路由定義unmatchedPath將爲 /user/page安全

pathPrefix("api") { ctx =>
    // ctx.unmatchedPath 等價於 "/user/page"
    ctx.complete(ctx.request.uri.path.toString())
  }

RouteResult

RouteResult是一個簡單的ADT(抽象數據類型),對路由執行後可能的結果進行建模,定義爲:app

sealed trait RouteResult extends javadsl.server.RouteResult

object RouteResult {
  final case class Complete(response: HttpResponse) extends javadsl.server.Complete with RouteResult {
    override def getResponse = response
  }
  final case class Rejected(rejections: immutable.Seq[Rejection]) extends javadsl.server.Rejected with RouteResult {
    override def getRejections = rejections.map(r => r: javadsl.server.Rejection).toIterable.asJava
  }
}

一般不須要咱們直接建立RouteResult實例,而是經過預約義的指令RouteDirectives定義的函數(completerejectredirectfail)或RequestContext上的方法來建立。ide

組合路由

將單個的路由組合成一個複雜的路由結構通常有3種方法:

  1. 路由轉換(嵌套),將請求委託給另外一個「內部」路由,在此過程當中能夠更改傳請求和輸出結果的某些屬性。
  2. 過濾路由,只容許知足給定條件的路由經過。
  3. 連接路由,若給定的第一個路由被拒絕(reject),將嘗試第二個路由,並依次類推。經過級聯操做符~來實現,導入akka.http.scaladsl.server.Directvies._後可用。

前兩種方法可由指令(Directive)提供,Akka HTTP已經預告定義了大量開箱即用的指令,也能夠自定義咱們本身的指令。經過指令這樣的機制,使得Akka HTTP的路由定義異常強大和靈活。

路由樹

當經過嵌套和連接將指令和自定義路由組合起來構建成一個路由結構時,將造成一顆樹。當一個HTTP請求進入時,它首先被注入的樹的根,並以深刻優先的方式向下流徑全部分支,直到某個節點完成它(返回Future[RouteResult.Complete])或者徹底拒絕它(返回Future[RouteResult.Rejected])。這種機制可使複雜的路由匹配邏輯能夠很是容易的實現:簡單地將最特定的狀況放在前面,而將通常的狀況放在後面。

val route =
  a {
    b {
      c {
        ... // route 1
      } ~
      d {
        ... // route 2
      } ~
      ... // route 3
    } ~
    e {
      ... // route 4
    }
  }

上面這個例子:

  • route 1 只有當a、b、c都經過時纔會到達。
  • route 2 只有當a、b經過,但c被拒絕時纔會到達。
  • route 3 只有當a、b經過,但c、d和它以前的全部連接的路由都被拒絕時纔會到達。
    • 能夠被看做一個捕獲全部(catch-all)的默認路由,以後會看到咱們將利用此特性來實現服務端對SPA前端應用的支持。
  • route 4 只有當a經過,b和其全部子節點都被拒絕時纔會到達。

Directive 指令

指令 是用於建立任意複雜路由結構的小型構建塊,Akka HTTP已經預先定義了大部分指令,固然咱們也能夠很輕鬆的定義本身的指令。

指令基礎

經過指令來建立路由,須要理解指令是如何工做的。咱們先來看看指令和原始的Route的對比。由於Route只是函數的類型別名,全部Route實例能夠任何方式寫入函數實例,如做爲函數文本:

val route: Route = { ctx => ctx.complete("yeah") }  // 或者可簡寫爲:_.complete("yeah")

complete指令將變得更短:

val route: Route = complete("yeah")

complete指令定義以下:

def complete(m: => ToResponseMarshallable): StandardRoute =
  StandardRoute(_.complete(m))

abstract class StandardRoute extends Route {
  def toDirective[L: Tuple]: Directive[L] = StandardRoute.toDirective(this)
}

object StandardRoute {
  def apply(route: Route): StandardRoute = route match {
    case x: StandardRoute => x
    case x                => new StandardRoute { def apply(ctx: RequestContext) = x(ctx) }
  }
}

指令能夠作什麼?

指令用來靈活、高效的構造路由結構,簡單來講它能夠作以下這些事情:

  1. Route傳入的請求上下文RequestContext轉換爲內部路由須要的格式(修改請求)。

    mapRequest(request => request.withHeaders(request.headers :+ RawHeader("custom-key", "custom-value")))
  2. 根據設置的邏輯來過濾RequestContext,符合的經過(pass),不符合的拒絕(reject)。

    path("api" / "user" / "page")
  3. RequestContext中抽取值,並使它在內部路徑內的路由可用。

    extract(ctx => ctx.request.uri)
  4. 定義一些處理邏輯附加到Future[RouteRoute]的轉換鏈上,可用於修改響應或拒絕。

    mapRouteResultPF {
      case RouteResult.Rejected(_) =>
        RouteResult.Complete(HttpResponse(StatusCodes.InternalServerError))
    }
  5. 完成請求(使用complete

    complete("OK")

指令已經包含了路由(Route)能夠用的全部功能,能夠對請求和響應進行任意複雜的轉換處理。

組合指令

Akka HTTP提供的Routing DSL構造出來的路由結構是一顆樹,因此編寫指令時一般也是經過「嵌套」的方式來組裝到一塊兒的。看一個簡單的例子:

val route: Route =
  pathPrefix("user") {
    pathEndOrSingleSlash { // POST /user
      post {
        entity(as[User]) { payload =>
          complete(payload)
        }
      }
    } ~
      pathPrefix(IntNumber) { userId =>
        get { // GET /user/{userId}
          complete(User(Some(userId), "", 0))
        } ~
          put { // PUT /user/{userId}
            entity(as[User]) { payload =>
              complete(payload)
            }
          } ~
          delete { // DELETE /user/{userId}
            complete("Deleted")
          }
      }
  }

Full source at GitHub

Akka HTTP提供的Routing DSL以樹型結構的方式來構造路由結構,它與 Playframework 和 Spring 定義路由的方式不太同樣,很難說哪種更好。也許剛開始時你會不大習慣這種路由組織方式,一但熟悉之後你會認爲它很是的有趣和高效,且很靈活。

能夠看到,若咱們的路由很是複雜,它由不少個指令組成,這時倘若還把全部路由定義都放到一個代碼塊裏實現就顯得很是的臃腫。由於每個指令都是一個獨立的代碼塊,它經過函數調用的形式組裝到一塊兒,咱們能夠這樣對上面定義的路由進行拆分。

val route1: Route =
  pathPrefix("user") {
    pathEndOrSingleSlash {
      post {
        entity(as[User]) { payload =>
          complete(payload)
        }
      }
    } ~
      pathPrefix(IntNumber) { userId =>
        innerUser(userId)
      }
  }

def innerUser(userId: Int): Route =
  get {
    complete(User(Some(userId), "", 0))
  } ~
    put {
      entity(as[User]) { payload =>
        complete(payload)
      }
    } ~
    delete {
      complete("Deleted")
    }

Full source at GitHub

經過&操做符將多個指令組合成一個,全部指令都符合時經過。

val pathEndPost: Directive[Unit] = pathEndOrSingleSlash & post

val createUser: Route = pathEndPost {
  entity(as[User]) { payload =>
    complete(payload)
  }
}

Full source at GitHub

經過|操做符將多個指令組合成一個,只要其中一個指令符合則經過。

val deleteEnhance: Directive1[Int] =
  (pathPrefix(IntNumber) & delete) | (path(IntNumber / "_delete") & put)

val deleteUser: Route = deleteEnhance { userId =>
  complete(s"Deleted User, userId: $userId")
}

Full source at GitHub

Note

上面這段代碼來自真實的業務,由於某些落後於時代的安全緣由,網管將HTTP的PUT、DELETE、HEAD等方法都禁用了,只保留了GET、POST兩個方法。使用如上的技巧能夠同時支持兩種方式來訪問路由。

還有一種方案來解決這個問題

val deleteUser2 = pathPrefix(IntNumber) { userId =>
  overrideMethodWithParameter("httpMethod") {
    delete {
      complete(s"Deleted User, userId: $userId")
    }
  }
}

Full source at GitHub

客戶端不須要修改訪問地址爲 /user/{userId}/_delete,它只須要這樣訪問路由 POST /user/{userId}?httpMethod=DELETEoverrideMethodWithParameter("httpMethod")會根據httpMethod參數的值來將請求上下文裏的HttpRequest.method轉換成 DELETE 方法請求。

Warning

能夠看到,將多個指令組合成一個指令能夠簡化咱們的代碼。可是,若過多地將幾個指令壓縮組合成一個指令,可能並不會獲得易讀、可維護的代碼。

使用concat來鏈接多個指令

除了經過~連接操做符來將各個指令鏈接起來造成路由樹,也能夠經過concat指令來將同級路由(指令)鏈接起來(子路由仍是須要經過嵌套的方式組合)。

val route: Route = concat(a, b, c) // 等價於 a ~ b ~ c

類型安全的指令

當使用&|操做符組合多個指令時,Routing DSL將確保其定期望的方式工做,而且還會在編譯器檢查是否知足邏輯約束。下面是一些例子:

val route1 = path("user" / IntNumber) | get // 不能編譯
val route2 = path("user" / IntNumber) | path("user" / DoubleNumber) // 不能編譯
val route3 = path("user" / IntNumber) | parameter('userId.as[Int]) // OK

// 組合指令同時從URI的path路徑和查詢參數時獲取值
val pathAndQuery = path("user" / IntNumber) & parameters('status.as[Int], 'type.as[Int])
val route4 = pathAndQuery { (userId, status, type) =>
    ....
  }

指令類型參數裏的 Tuple (自動拉平 flattening)

abstract class Directive[L](implicit val ev: Tuple[L])

type Directive0 = Directive[Unit]
type Directive1[T] = Directive[Tuple1[T]]

指令的定義,它是一個泛型類。參數類型L須要可轉化成akka.http.scaladsl.server.util.Tuple類型(即Scala的無組類型,TupleX)。下面是一些例子,DSL能夠自動轉換參數類型爲符合的Tuple

val futureOfInt: Future[Int] = Future.successful(1)
val route =
  path("success") {
    onSuccess(futureOfInt) { //: Directive[Tuple1[Int]]
      i => complete("Future was completed.")
    }
  }

onSuccess(futureOfInt)將返回值自動轉換成了Directive[Tuple1[Int]],等價於Directive1[Int]

val futureOfTuple2: Future[Tuple2[Int,Int]] = Future.successful( (1,2) )
val route =
  path("success") {
    onSuccess(futureOfTuple2) { //: Directive[Tuple2[Int,Int]]
      (i, j) => complete("Future was completed.")
    }
  }

onSuccess(futureOfTuple2)返回Directive1[Tuple2[Int, Int]],等價於Directive[Tuple1[Tuple2[Int, Int]]]。但DSL將自動轉換成指令Directive[Tuple2[Int, Int]]以免嵌套元組。

val futureOfUnit: Future[Unit] = Future.successful( () )
val route =
  path("success") {
    onSuccess(futureOfUnit) { //: Directive0
      complete("Future was completed.")
    }
  }

對於Unit,它比較特殊。onSuccess(futureOfUnit)返回Directive[Tuple1[Unit]]。DSL將會自動轉換爲Directive[Unit],等價於Directive0

本文節選自《Scala Web開發》,原文連接:http://www.yangbajing.me/scala-web-development/server-api/routing-dsl/index.html

相關文章
相關標籤/搜索