文档章节

Akka HTTP Routing DSL

羊八井
 羊八井
发布于 10/11 23:52
字数 2633
阅读 23
收藏 0

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

© 著作权归作者所有

共有 人打赏支持
羊八井

羊八井

粉丝 93
博文 38
码字总数 47266
作品 0
渝北
技术主管
私信 提问
Akka v2.4.5_2.12-M4 发布,Actor 模型开发库

Akka v2.4.5_2.12-M4 发布了,Akka 是一个用 Scala 编写的库,用于简化编写容错的、高可伸缩性的 Java 和 Scala 的 Actor 模型应用。 本次发布值得关注的内容: The cluster client and the...

oschina
2016/05/18
2.3K
3
Scala Web开发-Akka HTTP中使用JSON

Jackson Jackson 是Java生态圈里最流行的JSON序列化库,它的官方网站是:https://github.com/FasterXML/jackson。 为什么选择 Jackson 为什么选择 Jackson 而不是更Scala范的 play-json、 ci...

羊八井
10/09
0
0
Akka实战:构建REST风格的微服务

使用Akka-Http构建REST风格的微服务,服务API应尽量遵循REST语义,数据使用JSON格式交互。在有错误发生时应返回:类似的JSON错误消息。 代码: https://github.com/yangbajing/akka-action ...

羊八井
2015/11/27
854
0
基于Akka-Streams的HTTP代理的实现

Akka-Streams是一个让人激动的Reactive Streams的框架,Akka-Http也是构建在其之上,除了内置背压模式的支持,使用其DSL构建一个Graph也是一个让人惊艳的过程。对于Akka-Streams的介绍会在后...

bluishglc
2017/02/18
0
0
Akka HTTP:自定义指令(Directive)

自定义指令 有3种创建自定义指令的基本方法: 将已有指令通过命名配置(比如通过组合的方式)的方式来定义新的指令 转换已存在的指令 从头开始实现一个指令 命名配置 创建自定义指令最简便的...

羊八井
10/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

js垃圾回收机制和引起内存泄漏的操作

JS的垃圾回收机制了解吗? Js具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。 JS中最常见的垃圾回收方式是标记清除。 工作原理:是当变量进入环境时,将这个变量标记为“...

Jack088
15分钟前
1
0
大数据教程(10.1)倒排索引建立

前面博主介绍了sql中join功能的大数据实现,本节将继续为小伙伴们分享倒排索引的建立。 一、需求 在很多项目中,我们需要对我们的文档建立索引(如:论坛帖子);我们需要记录某个词在各个文...

em_aaron
32分钟前
2
0
"errcode": 41001, "errmsg": "access_token missing hint: [w.ILza05728877!]"

Postman获取微信小程序码的时候报错, errcode: 41001, errmsg: access_token missing hint 查看小程序开发api指南,原来access_token是直接当作parameter的(写在url之后),scene参数一定要...

两广总督bogang
32分钟前
6
0
MYSQL索引

索引的作用 索引类似书籍目录,查找数据,先查找目录,定位页码 性能影响 索引能大大减少查询数据时需要扫描的数据量,提高查询速度, 避免排序和使用临时表 将随机I/O变顺序I/O 降低写速度,占用磁...

关元
50分钟前
7
0
撬动世界的支点——《引爆点》读书笔记2900字优秀范文

撬动世界的支点——《引爆点》读书笔记2900字优秀范文: 作者:挽弓如月。因为加入火种协会的读书活动,最近我连续阅读了两本论述流行的大作,格拉德威尔的《引爆点》和乔纳伯杰的《疯传》。...

原创小博客
今天
18
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部