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