构建一个简单的 Spray 应用

栏目: Scala · 发布时间: 7年前

内容简介:构建一个简单的 Spray 应用

带你轻松入坑spray

前段时间使用 scala 构建了一个略微复杂的业务系统, web 框架选用了 spray, 参考了这个文档, 就顺带翻译了, 当做一个简明的入坑教程, 多少为scala的普及做点贡献。

正文

你可能已经从别的地方听说过spray了, 也有可能你在 解析json的时候用到了spray。 其实Spray可以让你以极简的方式实现一个 rest 服务, 上手一个新框架最难的就是去了解其中的一些概念, 我用一个简单的应用带你去搞定spray,学会一些最常用的功能。

Spray的文档中对于 依赖部分和 imports 哪些类可能讲的不多, 我这里全都引用好, 免得你还得去找。

写一个项目的 build.sbt 文件, 加上以下的依赖

val akka = "2.3.9"

val spray = "1.3.2"

resolvers += Resolver.url("TypeSafe Ivy releases", url("http://dl.bintray.com/typesafe/ivy-releases/"))(Resolver.ivyStylePatterns)

libraryDependencies ++=
    Seq(
        // -- Logging --
        "ch.qos.logback" % "logback-classic" % "1.1.2",
        "com.typesafe.scala-logging" %% "scala-logging-slf4j" % "2.1.2",
        // -- Akka --
        "com.typesafe.akka" %% "akka-testkit" % akka % "test",
        "com.typesafe.akka" %% "akka-actor" % akka,
        "com.typesafe.akka" %% "akka-slf4j" % akka,
        // -- Spray --
        "io.spray" %% "spray-routing" % spray,
        "io.spray" %% "spray-client" % spray,
        "io.spray" %% "spray-testkit" % spray % "test",
        // -- json --
        "io.spray" %% "spray-json" % "1.3.1",
        // -- config --
        "com.typesafe" % "config" % "1.2.1",
        // -- testing --
        "org.scalatest" %% "scalatest" % "2.2.1" % "test"
    )

创建一个干净的空壳服务

spray 使用 akka actors 来接收服务调用,  一种设置路由的方式是创建 scala 的 traits,  如下:

import akka.actor.Actor
import spray.routing.HttpService

class SampleServiceActor extends Actor with SampleRoute {
    def actorRefFactory = context
    def receive = runRoute(route)
}

trait SampleRoute extends HttpService {
    val route = {
        get {
            complete("I exist!")
        }
    }
}

你可以看到, 我们创建了一个定义路由的 trait, 并且吧路由传入到 runRoute里, 你不一定要使用这种方式, 但是这样能更好的组织你的路由。

到现在, 我们有了一个完备的service, 让我们运行起来玩一玩, 我们需要一个main class 来绑定端口

import akka.actor.{ActorSystem, Props}
import akka.io.IO
import spray.can.Http

object Main extends App {
    implicit val system = ActorSystem("simple-service")
    val service = system.actorOf(Props[SampleServiceActor], "simple-service")

    IO(Http) ! Http.Bind(service, host, port = port)
}

我们的main 函数里面构造了一个   Akka ActorSystem, 并且把我们的  SampleServiceActor 跑在 系统里,

这个时候你就可以 在 IntelliJ 里启动main 函数了, 启动后, 可以去浏览器访问  http://localhost:8080/ 里面看一下有什么东西。

现在我们有了一个基本的壳子, 我们来干点有趣的。

增加一个 paths/routes

我们现在直接在根路径下添加一个 stuff 的路由。

val route = {
        get {
            complete("I exist!")
        } ~
          get {
              path("stuff") {
                  complete("That's my stuff!")
              }
          }
    }

path 指令允许我们定义一个路由, ~ 符号可以把多个路由串起来(这是spray里面常用的做法), http://localhost:8080/stuff 按理应该展示 That's my stuff! ,  然而却显示了 I exist! .

其实这里的问题是, spray 会依次往下匹配, 先匹配上了 / , 所以就解析到这个路径了, 我们可以重构一下

val route = {
        get {
            path("stuff") {
                complete("That's my stuff!")
            }
        } ~ get {
            complete("I exist!")
        }
    }

这样就可以了

发送一个 POST 请求

你可能注意到了, get 这个关键字代表着一种动作, 当然我们可以使用其他的动词(post/put/delete/etc), 我们发送一个 post请求,

val route = {
        get {
            path("stuff") {
                complete("That's my stuff!")
            }
        } ~
          post {
              path("stuff") {
                  complete("stuff posted!")
              }
          } ~
          get {
              complete("I exist!")
          }
    }

我们现在可以 post一个消息, 并且从响应中看到这条消息, 但是还有点小问题,  我们在 get 和post 中使用了两次 相同的path,

spray 有很灵活的 DSL, 你可以随意调整 path 的位置,   我们可以很轻松去掉重复的代码, 只需要把 path 放在外面就可以了,里面包含 get 和post 方法。

val route = {
        path("stuff") {
            get {
                complete("That's my stuff!")
            } ~
              post {
                  complete("stuff posted!")
              }
        } ~
          get {
              complete("I exist!")
          }
    }

这样是不是好一点了,

读取和返回 json 数据

如果我们需要处理一下数据, 我们就需要用到 json 对象了,

我们这里就用一些简单的json 数据, 我这里用  spray json 来做序列化。

import spray.json.DefaultJsonProtocol

case class Stuff(id: Int, data: String)

object Stuff extends DefaultJsonProtocol {
    implicit val stuffFormat = jsonFormat2(Stuff.apply)
}

我们来返回一些测试数据

trait SampleRoute extends HttpService {
    import spray.httpx.SprayJsonSupport._
    import Stuff._
    import spray.http.MediaTypes

    val route = {
        path("stuff") {
            respondWithMediaType(MediaTypes.`application/json`) {
                get {
                    complete(Stuff(1, "my stuff"))
                } ~
                  post {
                      complete("stuff posted!")
                  }
            }
        } ~
          get {
              complete("I exist!")
          }
    }
}

这里 complete 方法处理 Stuff 的case  class 类, 并且自动 把数据序列化为了 json。 注意这里的 respondWithMediaType 方法, 如果期望获取 XML 类型的数据, 你可以使用这个方法来构造路由。

现在我们返回了数据, 我们稍微改一下, 让我们的接口可以读取 Stuff 类型的数据,  我们在 id 上面加100, 并且返回。

post {
    entity(as[Stuff]) { stuff =>
        complete(Stuff(stuff.id + 100, stuff.data + " posted"))
    }
}

这里我们可以看到, 我们使用 entity(as[Stuff]) 加了一层嵌套,   这个指令会解析json数据到一个 对象里面, 并且把这个对象当参数传到你的代码块中。

多层路由

你可能需要处理 类似 junk/mine 和 junk/yours 这种的路由, 直觉可能写成  path(“junk/mine”), 但是这样不行, 当你需要复杂一点的路由的时候, spray 提供了  pathPrefix 和 pathEnd 指令

pathPrefix("junk") {
     pathPrefix("mine") {
        pathEnd {
            get {
                complete("MINE!")
            }
         }
    } ~ pathPrefix("yours") {
        pathEnd {
            get {
                complete("YOURS!")
            }
        }
    }
}

这里可以嵌套很多层, 所以是极其灵活的, 你可以构建很复杂的路由, 当然我们这里只举一些简单的例子。

获取 query 参数

有时候,我们需要获取query 参数, 有必填的参数, 也有和选填的参数, 这个时候我们可以使用 parameters 指令。

path("params") {
    get {
        parameters('req, 'opt.?) { (req, opt) =>
            complete(s"Req: $req, Opt: $opt")
        }
    }
}

注意这里的 ? 符合, 这代表前面的这个参数是选填的, 否则是必填的。  这里你可以加很多参数,都作为变量 传到后面的代码块中, 如果你的 请求是

http://localhost:8080/params?req=hi&opt=bye

那么返回的应该是

Req: hi, Opt: Some(bye)

这里的 选填的参数是 scala Option 类型的,

这样, 如果你的请求一个参数都没加(http://localhost:8080/params), 那么匹配的就是根路径, 因为这里有一个必填的参数,你不带,就匹配不到这里的路径。

Request is missing required query parameter 'req'

获取 headers

类似于获取query 参数, 我们这里使用 headerValueByName 指令来获取headers

path("headers") {
    get {
        headerValueByName("ct-remote-user") { userId =>
            complete(userId)
        }
    }
}

这里也是 Option 类型,

返回一个 futures

你的服务可能会调用 一些响应式的框架, 比如 Slick 3 , 或者你想直接返回 scala 的future 类型, 恭喜你, complete 也是支持的。

path("reactive") {
    get {
        complete(future { "I'm reactive!" })
    }
}

这样你的代码就不需要  Await.result! 了, 是不是很爽

打包部署

我们现在有了一个完整的应用,我们来部署上线,  我们使用 sbt 来构建发布,

lazy val root = (project in file(".")).enablePlugins(JavaAppPackaging)

我们使用了  sbt-native-packager 插件, 需要 在  project/plugins.sbt 文件中加一下

addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0")

这样, 你运行 sbt dist 命令就可以在 target/universal 目录下发现一个 zip 目标文件, 你就可以用来发布了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Practice of Programming

The Practice of Programming

Brian W. Kernighan、Rob Pike / Addison-Wesley / 1999-2-14 / USD 49.99

With the same insight and authority that made their book The Unix Programming Environment a classic, Brian Kernighan and Rob Pike have written The Practice of Programming to help make individual progr......一起来看看 《The Practice of Programming》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具