框架 | spray-routing的核心流程

最近我们在一个项目上使用spray来发布restful service。

spray是个性能很好而且功能非常完整的service框架,包含很多组件,从底层http服务器到高层的rest路由DSL都有。一般简单的应用就使用和掌握好最高层的spray-routing就够用。本文主要讲spray-routing,不及其余。 spray整体的设计理念,spray和akka的关系留待以后的博客。

spray-routing上手很容易,但是有一些比较独特的概念和设计。如果没有一定的理解,就会发现当系统复杂到一定程度时对于有些需求不知道该怎么实现了。为方便大家掌握使用,本文主要解释了spray的核心流程,而不涉及深入讲解最核心的Directive(指令)。

spray发布http service的流程如下:

整个流程由spray框架控制,http连接处理由spray-can或spray-servlet负责,大部分情况下,开发人员只要定义路由——url和业务服务的映射——以及对应的业务服务即可,注意这个路由定义并不是一个配置文件,而是spray-routing定义的一套scala的DSL。

请求到达时,spray会先查找路由定义,如果请求的URL没有找到最终能完成请求的服务则会拒绝(reject)。 如果找到,则spray会根据你在路由定义里的配置,把请求参数转成业务对象(比如用json4s把json请求转换成scala对象,需要用Entity指令来定义),然后调用业务服务。调用可能有三种结果:

  • 业务处理正常返回,则将返回的业务对象根据配置的转换方式转换回HttpResponse,再返回给客户端
  • 调用业务服务超时,则交由一个可覆盖的超时处理器处理,默认实现是返回500内部服务器错,并带一个默认的错误信息
        def timeoutRoute: Route = complete(
              InternalServerError,
              "The server was not able to produce a timely response to your request.")
  • 业务服务抛异常,跟超时处理一样会被交给一个可自定义的异常处理块去统一处理

我们的路由服务一般继承HttpService,HttpService继承自HttpServiceBase,其中提供了runRoute方法,也就是路由的入口。

def runRoute(route: Route)(implicit eh: ExceptionHandler, rh: RejectionHandler, ac: ActorContext,rs: RoutingSettings, log: LoggingContext): Actor.Receive

runRoute的参数是Route:

type Route = RequestContext ⇒ Unit

是RequestContext => Unit的类型别名

我们按照spray例子写的path("xx")...其实就是定义了一个RequestContext => Unit的函数,也就是如何从请求上下文里解析请求内容,调用业务服务。 比较奇怪的是返回类型是Unit,spray会调用RequestContext里包含的responder成员来负责将响应返回给客户端。 据spray-routing文档里说是为了”non-blocking"和"actor friendly",但实际上在spray的后续版本,也就是akka-http里把这个返回类型改成了RouteResult,也就是RequestContext => RouteResult,这样感觉更合理,更对应于HttpRequest => HttpResponse的本质。

我们完全可以定义一个RequestContext ⇒ Unit类型的路由,然后自己从RequestContext里解析出请求数据,自己做数据转换,自己决定应该调用什么服务(实际上有些时候我们确实要这么做)。 但大部分时候我们可以用spray-routing通过一组Directive——翻译成中文就是指令——提供的路由DSL来定义我们的路由。这也是spray-routing提供的最核心的功能。

比如官方文档里的路由例子:

import spray.routing._
import Directives._
val route: Route =
  path("order" / IntNumber) { id =>
    get {
      complete {
        "Received GET request for order " + id
      }
    } ~
    put {
      complete {
        "Received PUT request for order " + id
      }
    }
  }

path、get、put、complete都是指令,spray的指令实现用了一种比较复杂的模式叫做磁铁模式(maget pattern)。

拿上面代码里的path为例,directive的一般形式为:

name(arguments) { extractions =>
  ... // inner Route
}

spray对RequestContext => Unit要完成的事情通过directive做了很好的抽象:

  • 转换——将RequestContext做一些转换再传给下一级路由
  • 过滤——拒绝不符合条件的请求
  • 抽取——从RequestContext里抽取一些信息,使之在下级路由中可用,比如上例中的id =>
  • 完成请求——比如上例中的complete{ }

对于过滤功能而言,还需要能“并联”——如果这个路径与请求不匹配,spray要去尝试下一个路径,有点像嵌套的模式匹配。 在spray-routing里并联用的是操作符 “~” 在前例中的get和put分支的并联可以看得很清楚。 但”~“不是唯一的把directive组合起来的方法,当路由定义变得庞大的时候,我们会需要某种方法把大量类似的结构抽取出来免得写出一棵巨大无比的路由树。

再回头看一下前面的流程图,除了正常路由、正常处理外还有拒绝,异常,超时三个分支。看上去好像我们只定义了正常处理的逻辑,实际上是我们的spray路由的入口runRoute这个方法偷偷做了默认处理:

def runRoute(route: Route)(implicit eh: ExceptionHandler, rh: RejectionHandler, ac: ActorContext,rs: RoutingSettings, log: LoggingContext): Actor.Receive

里面有隐式参数ExceptionHandler和RejectionHandler,以及一个隐藏的超时处理:

case Timedout(request: HttpRequest) ⇒ runRoute(timeoutRoute)(eh, rh, ac, rs, log)(request)

默认的拒绝实现对于常见的拒绝原因都给出正确的错误码和不错的返回信息,比如

case AuthorizationFailedRejection :: _ ⇒

complete(Forbidden, "The supplied authentication is not authorized to access this resource")

异常处理器和超时处理器也一样,如果你需要定制也可以定制自己的处理器,具体方法可以查阅Spray官方文档介绍的Reject Handler,Exception Handler和Timeout Handler。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2015-01-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java工会

反射在微信公众平台开发中的应用

1154
来自专栏智能计算时代

Envoy架构概览(2):HTTP过滤器,HTTP路由,gRPC,WebSocket支持,集群管理器

HTTP过滤器 就像网络级别的过滤堆栈一样,Envoy在连接管理器中支持HTTP级别的过滤堆栈。可以编写过滤器,在不知道底层物理协议(HTTP / 1.1,HT...

2946
来自专栏非典型技术宅

Swift多线程:使用GCD实现异步下载图片1. GCD基础知识2. GCD的基础应用3. GCD的服务质量(优先级)

1186
来自专栏散尽浮华

memcached缓存知识简单梳理

memcached工作原理 基本概念:slab,page,chunk。 slab,是一个逻辑概念。它是在启动memcached实例的时候预处理好的,每个slab...

1856
来自专栏步履前行

深入理解JVM--(1)运行时的数据区域划分-程序计数器

  最近在学习jvm,准备在园子里写个系列笔记,有什么问题大家可以一起探讨。今天学习数据区域划分的第一部分--程序计数器。   JVM在运行时会把管理的内存划...

3456
来自专栏智能大石头

NetCore版RPC框架NewLife.ApiServer

微服务和消息队列的基础都是RPC框架,比较有名的有WCF、gRPC、Dubbo等,我们的NewLife.ApiServer建立在网络库NewLife.Net之上...

570
来自专栏JetpropelledSnake

Python Web学习笔记之并发编程的孤儿进程与僵尸进程

1183
来自专栏冰霜之地

全双工通信的 WebSocket

WebSocket 是一种网络通信协议。在 2009 年诞生,于 2011 年被 IETF 定为标准 RFC 6455 通信标准。并由 RFC7936 补充规...

781
来自专栏程序员的SOD蜜

不使用反射,“一行代码”实现Web、WinForm窗体表单数据的填充、收集、清除,和到数据库的CRUD

问题篇:     昨天在CSDN看到这样一个帖子:“苦逼的三层代码”: 采用传统的三层架构写代码,每个数据表都要定义一个实体对象,编写后台的时候, Web层需要...

2848
来自专栏葡萄城控件技术团队

程序员级别鉴定书(.NET面试问答集锦)

作为一个.NET程序员,应该知道的不仅仅是拖拽一个控件到设计时窗口中。就像一个赛车手,一定要了解他的爱车 – 能做什么不能做什么。 本文参考Scott Hans...

2347

扫描关注云+社区