type Route = RequestContext => Future[RouteResult]
Akka HTTP 里路由是類型 Route
只是一個類型別名,它其實是一個函數 RequestContext => Future[RouteResult]
,它接受一個 RequestContext
參數,並返回 Future[RouteResult]
。RequestContext
保存了每次HTTP請求的上下文,包括HttpRequest
、unmatchedPath
、settings
等請求資源,還有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
包裝了HTTP請求的實例HttpRequest
和運行時須要的一些上下文信息,如:ExcutionContext
、Materializer
、LoggingAdapter
、RoutingSettings
等,還有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
是一個簡單的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
定義的函數(complete
、reject
、redirect
、fail
)或RequestContext
上的方法來建立。ide
將單個的路由組合成一個複雜的路由結構通常有3種方法:
~
來實現,導入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 } }
上面這個例子:
指令 是用於建立任意複雜路由結構的小型構建塊,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) } } }
指令用來靈活、高效的構造路由結構,簡單來講它能夠作以下這些事情:
將Route
傳入的請求上下文RequestContext
轉換爲內部路由須要的格式(修改請求)。
mapRequest(request => request.withHeaders(request.headers :+ RawHeader("custom-key", "custom-value")))
根據設置的邏輯來過濾RequestContext
,符合的經過(pass),不符合的拒絕(reject)。
path("api" / "user" / "page")
從RequestContext
中抽取值,並使它在內部路徑內的路由可用。
extract(ctx => ctx.request.uri)
定義一些處理邏輯附加到Future[RouteRoute]
的轉換鏈上,可用於修改響應或拒絕。
mapRouteResultPF { case RouteResult.Rejected(_) => RouteResult.Complete(HttpResponse(StatusCodes.InternalServerError)) }
完成請求(使用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") } } }
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") }
經過&
操做符將多個指令組合成一個,全部指令都符合時經過。
val pathEndPost: Directive[Unit] = pathEndOrSingleSlash & post val createUser: Route = pathEndPost { entity(as[User]) { payload => complete(payload) } }
經過|
操做符將多個指令組合成一個,只要其中一個指令符合則經過。
val deleteEnhance: Directive1[Int] = (pathPrefix(IntNumber) & delete) | (path(IntNumber / "_delete") & put) val deleteUser: Route = deleteEnhance { userId => complete(s"Deleted User, userId: $userId") }
Note
上面這段代碼來自真實的業務,由於某些落後於時代的安全緣由,網管將HTTP的PUT、DELETE、HEAD等方法都禁用了,只保留了GET、POST兩個方法。使用如上的技巧能夠同時支持兩種方式來訪問路由。
還有一種方案來解決這個問題
val deleteUser2 = pathPrefix(IntNumber) { userId => overrideMethodWithParameter("httpMethod") { delete { complete(s"Deleted User, userId: $userId") } } }
客戶端不須要修改訪問地址爲 /user/{userId}/_delete
,它只須要這樣訪問路由 POST /user/{userId}?httpMethod=DELETE
。overrideMethodWithParameter("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) => .... }
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