原 薦 Akka HTTP:自定義指令(Directive)
自定義指令
有3種建立自定義指令的基本方法:
- 將已有指令通過命名配置(比如通過組合的方式)的方式來定義新的指令
- 轉換已存在的指令
- 從頭開始實現一個指令
命名配置
建立自定義指令最簡便的方法就是將一個或多個已有指令通過配置的方式分配一個新的名字來定義。事實上Akka HTTP預定義的大多數指令都由以較低級別指令命名配置的方式來定義的。如:
val getPut = get & put def postEntity[T](um: FromRequestUnmarshaller[T]): Directive1[T] = post & entity(um) def completeOk: Route = complete(HttpEntity.Empty) def completeNotImplemented: Route = complete(StatusCodes.NotImplemented)
轉換已存在的指令
第二種方式是通過“轉換方法”來轉換現有指令,這是在Directive
類上定義的方法:
map/tmap flatMap/tflatMap require/trequire recover/recoverPF
map、tmap
map、tmap
就和Scala集合庫上的map
轉換類似,它可以將值對映轉換成另一個值。map
用於Directive1
型別的指令(單值指令),而tmap
用於值為其它元組的情況,它的簽名如下:
def tmap[R](f: L => R): Directive[Out]
tmap
可以用來將提取的元組轉換成另一個元組,提取的數量和型別都可以改變,而map
只用改變變換後的型別。如下是一個虛構的例子:
val twoIntParameters: Directive[(Int, Int)] = parameters(("a".as[Int], "b".as[Int])) val myDirective: Directive1[String] = twoIntParameters.tmap { case (a, b) => (a + b).toString } // tests: Get("/?a=2&b=5") ~> myDirective(x => complete(x)) ~> check { responseAs[String] mustBe "7" }
flatMap、tflatMap
通過map、tmap
可以將指令抽取的值轉換成其它值,但不能改變其“抽取”的性質。當需要抽取一個對它做一些轉換操作,並將結果交給一個巢狀的指令使用時,map、tmap
就無能為力了。同map、tmap
類似,flatMap
也是用於單值指令,而tflatMap
用於其它元組值。tflatMap
的函式簽名如下:
def tflatMap[R: Tuple](f: L => Directive[R]): Directive[R]
可以看一個例子,預定義的method
指令,它的定義如下:
def method(httpMethod: HttpMethod): Directive0 = extractMethod.flatMap[Unit] { case `httpMethod` => pass case _=> reject(MethodRejection(httpMethod)) } & cancelRejections(classOf[MethodRejection]) val get: Directive0 = method(HttpMethods.GET) val post: Directive0 = method(HttpMethods.POST)
-
通過呼叫
extractMethod
指令獲取請求的HTTP方法,再通過flatMap[Unit]
轉換方法對它進行處理。因為extractMethod
是一個單值指令且轉換後值為Unit
(也是個單值),這裡呼叫flatMap
方法。 -
當請求的實際HTTP方法與傳入引數
httpMethod
匹配時,呼叫pass
指令使其通過,否則呼叫reject(MethodRejection(httpMethod))
拒絕。
require、trequire
require方法將單個指令轉換為沒有抽取值的指令,該指令根據謂詞函式過濾請求,所有謂詞函式呼叫後為false的請求都被拒絕,其它請求保持不變。它的定義如下:
def require(predicate: T => Boolean, rejections: Rejection*): Directive0 = underlying.filter(predicate, rejections: _*).tflatMap(_ => Empty)
從定義可以看出,它實際上是先通過謂詞函式呼叫filter
方法對請求進行過濾,然後再呼叫tflatMap
函式將指令抽取的值去掉。
recover、recoverPF
recover方法允許“捕獲”由底層指令向上冒泡產生的rejections,並生成且有相同抽取型別的替代指令。這樣就可以恢復指令來通過而不是拒絕它。它們的定義分別如下:
def recover[R >: L: Tuple](recovery: immutable.Seq[Rejection] => Directive[R]): Directive[R] = Directive[R] { inner => ctx => import ctx.executionContext @volatile var rejectedFromInnerRoute = false tapply({ list => c => rejectedFromInnerRoute = true; inner(list)(c) })(ctx).fast.flatMap { case RouteResult.Rejected(rejections) if !rejectedFromInnerRoute => recovery(rejections).tapply(inner)(ctx) case x => FastFuture.successful(x) } } def recoverPF[R >: L: Tuple](recovery: PartialFunction[immutable.Seq[Rejection], Directive[R]]): Directive[R] = recover { rejections => recovery.applyOrElse(rejections, (rejs: Seq[Rejection]) => RouteDirectives.reject(rejs: _*)) }
從頭開始實現一個指令
可以通過呼叫Directive.apply
或它的子型別來從頭開始定義一個指令,Directive
的簡化定義看起來像下面這樣:
abstract class Directive[L](implicit val ev: Tuple[L]) { def tapply(f: L => Route): Route } object Directive { /** * Constructs a directive from a function literal. */ def apply[T: Tuple](f: (T => Route) => Route): Directive[T] = new Directive[T] { def tapply(inner: T => Route) = f(inner) } }
Directive
型別有一個抽象方法tapply
,引數f
是一個函式型別,將型別L
傳入並返回Route
。Directive
的伴身物件提供了apply
來實現自定義指令。它的引數是一個高階函式(T => Route) => Route
,就像小括號那樣,我們應把(T => Route)
看成一個整體,它是函式引數,返回型別為Route
。
f
為我們自定義指令用於從RequestContext
裡抽取值(值的型別為Tuple[L]
),而inner
就是f
抽取值後呼叫的巢狀路由,在呼叫inner
時將抽取出的值作為引數傳入。
對於一個抽取訪問host和port的指令,可以這樣實現:
def hostnameAndPort: Directive[(String, Int)] = Directive[(String, Int)] { inner => ctx => // inner: (String, Int) => Route // ctx: RequestContext val authority: Uri.Authority = ctx.request.uri.authority val tupleValue: (String, Int) = (authority.host.address(), authority.port) val route: Route = inner(tupleValue) route(ctx) // Future[RouteResult] }
ofollow,noindex" target="_blank">Full source at GitHub
讓我們來分析下這個例子:
-
首先是
hostnameAndPort
指令的型別Directive[(String, Int)]
,它從請求上下文(RequestContext
)中抽取出的值是Tuple2[String, Int]
。 -
apply
方法執行的程式碼引數是:inner => ctx => ....
其實可以看成:inner => ((ctx: RequestContext) => Future[RouteResult])
,inner
就是f
函式引數(T => Route)
部分。 -
inner(tupleValue)
執行後結果route
的型別是Route
,這時這段程式碼為的型別就為inner => ctx => Route
,而實際上Directive.apply
需要的引數型別為inner => Route
。之前我們知道,Route
是一個類型別名RequestContext => Future[RouteResult]
,所以我們需要將ctx => Route
轉換為Route
。而將tupleValue
作為引數呼叫route
後將獲取結果型別Future[RouteResult]
,這段程式碼的型別就是inner => ctx => Future[RouteResult]
->inner => Route
。
本文節選自《Scala Web開發》,更多內容請訪問:https://www.yangbajing.me/scala-web-development/server-api/routing-dsl/custom-directive.html 。