框架 | 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团长

Java后端程序员1年工作经验总结

  毕业已经一年有余,这一年里特别感谢技术管理人员的器重,以及同事的帮忙,学到了不少东西。这一年里走过一些弯路,也碰到一些难题,也受到过做为一名开发却经常为系统...

1712
来自专栏美团技术团队

Node.js Stream - 进阶篇

在构建较复杂的系统时,通常将其拆解为功能独立的若干部分。这些部分的接口遵循一定的规范,通过某种方式相连,以共同完成较复杂的任务。譬如,shell通过管道|连接各...

3964
来自专栏Java学习网

Java中UUID的2种创建方法——有代码实例

UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的字符串,它保证对在同一时空中的所有机器都是唯一的。按...

29410
来自专栏Java架构解析

深入源码分析 Java 线程池的实现原理

程序的运行,其本质上,是对系统资源(CPU、内存、磁盘、网络等等)的使用。如何高效的使用这些资源是我们编程优化演进的一个方向。今天说的线程池就是一种对CPU利用...

1200
来自专栏Java架构沉思录

不懂RPC,休谈微服务

在学校期间大家都写过不少程序,比如写个hello world服务类,然后本地调用下,如下所示。这些程序的特点是服务消费方和服务提供方是本地调用关系。

1192
来自专栏Java架构沉思录

优雅实现延时任务之Redis篇

PS:这篇文章昨天已经推送过了,不过忘了标原创,今天标个原创再发一次,昨天看了的可以不用往下看了。

1502
来自专栏owent

关于BUS通信系统的一些思考(二)

虽然我很不愿意再设计一套BUS系统,但是现有的一些确实都没有特别符合我的口味的。所以还是尝试设计一个出来。

923
来自专栏一枝花算不算浪漫

[Java面试七]Mybatis总结以及在面试中的一些问题.

46214
来自专栏安恒网络空间安全讲武堂

MOCTF WEB 题解

0x00 MOCTF平台是CodeMonster和Mokirin这两支CTF战队所搭建的一个CTF在线答题系统。网址是http://www.moctf.com/...

7289
来自专栏Java架构沉思录

优雅实现延时任务之Redis篇

延时任务,顾名思义,就是延迟一段时间后才执行的任务。举个例子,假设我们有个发布资讯的功能,运营需要在每天早上7点准时发布资讯,但是早上7点大家都还没上班,这个时...

1323

扫码关注云+社区