spray 是基于 akka 的轻量级 scala 库,可用于编写 REST API 服务。了解 spray 的 DSL 后可以在很短的时间内写出一个 REST API 服务,它的部署并不需要 tomcat , apche 等容器,可以直接 run。对于每一个 route, spray 都会 sprawn 一个或多个 actor提供服务,actor 的数目是可以配置的,我们不需要关心多线程处理的问题。从 benchmark 来看, spray 的性能是很不错。另外,spray 提供了一套测试套件,testkit,使用它可以在本地测试 API 的可用性,测试功能非常强大,我想它可以完全取代 SoapUI 之类的自动化测试工具
但是,学习使用spray的过程还是比较痛苦的,举个例子
Await.result(statement, 5.seconds)
写下这行代码后,IDE 会抱怨 5.seconds 理解不了,但是它在哪,IDE并不会提示。还有很多情况是,IDE 不会抱怨,但是编译过程会出错,说找不到某个 implicit 变量。接触spray三个月,最让我头疼的就是implicit变量的问题。关于常用的变量的头文件位置,我想写一篇日志记录一下
下面是一些概念性的理解,主要是参考 spray doc,附带部分自己的理解。
Route
type Route = RequestContext => Unit
RequestContext 在 route 里是局部变量,它总是存在的,我们可以在 route 的任何位置调用 RequestContext 的方法。RequestContext 包含 request 和 response 两层含义,既能获得request传过来的数据,也能完成.complete, .reject 等response语义。Route的定义并没有返回值,它的返回值为Unit,这是因为Route 采用的是 fire and forget (tell) 模式,它的好处首先是灵活,没有限制返回值的类型,其次是这种设计是完全的非阻塞的,易于和actor结合
Directive (指令)
spray的 DSL 就是一个个 Directive 拼在一起的
def routes = {
path("") {
get {
respondWithMediaType(`text/html`) {
complete {
DefaultValues.defaultAPI
}
}
}
}
}
path, get, respondWithMediaType, complete 都算是一条 Directive。他们嵌套在一起形成一个 Route 的语义。一般来讲,Directive有四个功能,首先是复制 RequestContext 到下一层 Directive,RequestContext 在传输的过程中是 immutable 的。其次,它可以获取 requester 附带的参数,比如 parameters, formData, jsonData 等,还能完成 marshall, unmarshall 操作。定义路径和某些逻辑来过滤一条请求,比如不符合任意一个 path 的请求将会被 reject,逻辑可以是 header 必须拒绝缓存,post data 只允许 json 格式等等。最后,他可以用返回结果到 requester,可以定义结果的类型和值,在上面的例子上就是 text/html 和 string(defaultAPI)
Reject, Exception, Timeout handling
requester的请求可能格式有错误,可能权限不足,也可能数据包丢失,因此 spray 需要 ”异常“处理。异常处理可以是显式的自定义声明,spray 也有默认的实现。对于那些不符合任何 path(或者符合 reject path), 传输数据出错,没有认证的请求,spray 会调用 reject handling 的实现,程序的执行过程中出现的问题会调用 exception handling定义的操作,比如对于除0异常的处理。 Timeout 最简单,它定义请求在多少时间没有应答就会返回超时错误。
spray 通过 complete(code, message) 实现,浏览器会显示 code, message。对那些比较普遍的错误,spray有自定义的code, message,而那些自定义的错误,我们要手写 code message。
Json support
上面提到directive的四种功能,其中就包括从requestContext中获取parameter和formData。除此之外,spray 还提供从 json 到 case class 的映射,这相当于一个轻量级的 ORM,让我们的逻辑代码写的更加美观。
object Json4sProtocol extends Json4sSupport {
implicit def json4sFormats: Formats = DefaultFormats
}
import models.Json4sProtocol._
json -> case class 有多个库可以选择,我更喜欢 json4s,相比于 spray.json,json4s的mapping更加简单,不需要为每一个case class写一个转换器。此外,json4s来支持json -> case class 的不完全转换,也就是说 json 可以有某些域不存在,但是 case class 对应的那些域必须声明为 Option[]
从 parameter 到 case class 也有映射,
case class Color(keyword: String, sort_order: Int, sort_key: String)
val testRoute =
path("test") {
parameters('keyword.as[String], 'sort_order.as[Int], 'sort_key.as[String]).as(Color) { color =>
//handleTestRoute(color) // route working with the Color instance
complete {
<h1>test route</h1>
}
}
}
spray test-kit
test-kit 能够实现本地测试 Route,配合 spec2 可以取代 soapUI等自动化测试工具。
"main entrance (/) working" in {
Get("/") ~> routes ~> check {
status === OK
response.entity.asString === DefaultValues.defaultAPI
}
}
step {
server.start()
server.createAndWaitForIndex("persons")
}
"Elasticsearch person dao" should {
"return None for a non-existing person" in new Context {
val person: Option[Person] = dao.get(UUID.randomUUID)
person must beNone
}
"create and then return a person" in new Context {
val id: UUID = dao.add(name = "John")
val person = dao.get(id)
person must beSome[Person]
person must haveName("John")
}
}
step {
server.stop()
}
test-kit 支持更丰富的语法糖,比如 beSome, beSome(data)等等,相比于 scalatest,spec2 提供更多的语法糖支持。