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

相关文章
相关标签/搜索