Akka HTTP Routing DSL

Route 路由

type Route = RequestContext => Future[RouteResult]

Akka HTTP 里路由是类型 Route 只是一个类型别名,它实际上是一个函数 RequestContext => Future[RouteResult],它接受一个 RequestContext 参数,并返回 Future[RouteResult]RequestContext保存了每次HTTP请求的上下文,包括HttpRequestunmatchedPathsettings等请求资源,还有4个函数来响应数据给客户端:

  • def complete(obj: ToResponseMarshallable): Future[RouteResult]:请求正常完成时调用,返回数据给前端。通过 Marshal 的方式将用户响应的数据类型转换成 HttpResponse,再赋值给RouteResult.Complete

  • def reject(rejections: Rejection*): Future[RouteResult]:请求不能被处理时调用,如:路径不存、HTTP方法不支持、参数不对、Content-Type不匹配等。也可以自定义Rejection类型。

  • def redirect(uri: Uri, redirectionType: Redirection): Future[RouteResult]:用指定的url地址和给定的HTTP重定向响应状态告知客户端需要重定向的地址和方式。redirect实际上是对complete的封装,可以通过向complete函数传入指定的HttpResponse实例实现:

    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来执行)。

RequestContext

RequestContext包装了HTTP请求的实例HttpRequest和运行时需要的一些上下文信息,如:ExcutionContextMaterializerLoggingAdapterRoutingSettings等,还有unmatchedPath,该值描述了请求UIR还未被匹配的路径。

unmatchedPath

若请求URI地址为:/api/user/page,对于如下路由定义unmatchedPath将为 /user/page

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

RouteResult

RouteResult是一个简单的ADT(抽象数据类型),对路由执行后可能的结果进行建模,定义为:

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上的方法来创建。

组合路由

将单个的路由组合成一个复杂的路由结构一般有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

转载于:https://my.oschina.net/yangbajing/blog/2243824

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值