启动一个Play Server
创建一个默认的SBT工程
Play现在支持以SBT的方式创建工程:
.
|-- build.sbt
|-- src/
|--main/
|--resources/
|--scala/
|-- mypackage/
|-- Main.scala
如果没有以存在的工程,可以按上面的主要格式创建一个工程.
在build.sbt中添加依赖
除了添加一些Play框架自身的依赖,从2.4版本开始需要添加Netty的依赖支持(注意:2.5版本中已经使用Akka-stream替换了Netty,如果需要使用Netty需要通过别的方式开启支持):
libraryDependencies += "com.typesafe.play" %% "play-netty-server" % "2.4.6"
添加 resources/application.conf 配置
Play仍然需要配置文件 application.conf
的支持,需要在resources目录下添加.一个主要的配置是需要提供 play.crypto.secret
:
play.crypto.secret = "test"
play.crypto.secret = ${?APPLICATION_SECRET}
启动NettyServer(2.4x)
从Main类启动服务:
val server = NettyServer.fromRouter() {
case GET(p"/posts/") => Action {
Results.Ok("All posts")
}
case GET(p"/posts/$id") => Action {
Results.Ok("Post:" + id )
}
}
这会以默认的host和port启动NettyServer并创建一个Nettyserver实例.
SIRD
SIRD的意思是 String Interprolating Routing DSL
, 上面的代码中已经进行了部分展示:
case GET(p"/posts/") =>
这种写法对于即便是没有Scala语言背景的人来说可读性也是很好的.上面的意思是: 接收一个HTTP的GET请求,url为 /posts
.
case GET(p"/posts/$id") =>
这样会匹配到一个 /posts/something
的URL,并把something部分解析为变量”id”.
解析查询参数
case GET(p"/posts" ? q"page=$p" & q"limit=$l") =>
这会把page和limit部分的参数分别以字符串类型解析为变量”p”和”l”,如果缺失任何其中一个参数,则会匹配失败.
可选参数
case GET(p"/posts" ? q"page=$p" & q_?"limit=$l") =>
符号”q_?”或者”q_o”用于解析可选参数,在URL匹配是作为可选参数,并且上面匹配到的l类型为 Option[String]
.
解析列表
Play支持以数组的方式解析URL中的重复参数,如, /posts?tag=tag1&tag=tag2&tag=tag3
:
case GET(p"/posts/" ? q_*"tag=$tags") =>
使用符号”q_*”或者”q_s”,可以把查询参数中所有的”tag”的值解析到变量”tags”,类型为 Seq[String]
.
如果URL中没有任何”tag”参数,这个URL仍然会匹配正确,得到的tags为一个空序列.
对正则表达式的支持
case GET(p"/posts/$id<[0-9]+>") =>
将所有数字匹配为id.
但是在查询字符串中不能使用正则
case GET(p"/posts/" ? q"id=$id<[0-9]{3}>") =>
这是一个查询字符串类型的URL,正则部分”<[0-9]{3}>”会得到一个编译错误:
Unexpected text at end of query string extractor: ‘<[0-9]{3}>’
有更好的方式在查询字符串类型的URL中匹配参数.
类型解析
case GET(p"/posts" ? q"page=${ int(p) }") =>
p 会被解析为一个Int类型,并且若果page是一个字母而不是数字时会匹配失败.
同时支持其他的基本类型,long,float,double,bool:
case GET(p"/posts/" ? q_?"limit=${ int(l) }" & q_*"xs=${ int(xs) }") =>
上面的模式会把l解析为 Option[Int]
,xs被解析为 Seq[Int]
.
同时支持path模式:
case GET(p"/posts/${ int(id) }" =>
如果需要添加更多验证,可以将类型解析和正则解析结合在一起:
case GET(p"/posts/${ int(id) }<[0-9]{3}>" ? q_*"tag=$tags") =>
或者直接使用模式匹配中的守卫:
case GET(p"/posts/${ int(id) }<[0-9]{3}>" ? q_*"tag=$tags") if tags.length > 0 =>
自定义解析器
如果有比较精练的验证需求,并且在多个route中重复,则可以定义自己的解析器:
case GET(p"/posts/${postId(id)}") =>
下面是具体实现方式:
case class PostId(id: String){ require(id.length == 3 } // 具体的验证需求
implicit object bindablePostId extends Parsing[PostId]( // 对require的异常捕获
PostId.apply,
_.id,
(key: String, e: Exception) => s"$key is not correct PostId"
)
val postId = new PathBindableExtractor[PostId] // 创建一个解析器实例
postID 是 PathBindableExtractor的实例,并且隐式的将bindablePostId作为参数,bindablePostId中对解析进行验证,并将PostID的实例作为一个值解析到id中.
如果参数不满足PostID的require部分,则该URL不会成功匹配,进而跳到下一个route进行匹配.不会有报错出现,异常部分也不会用到,在当前版本还不知道该异常的用途.
查询字符串组解析器
如果有多个查询字符串组总是同时出现,比如 page 和 limit 的组合,可以把他们组合在一起以实现DRY原则:
case class Pagination(page: Int, limit: Int) // 为了兼容Slick,可以使用Long
object pagination extends QueryStringParameterExtractor[Pagination] {
override def unapply(qs: QueryString) = qs match {
case q"page=${int(page)}" & q_?"limit=${int(limit)}" => // limit作为可选参数
Some(Pagination(page, limit.getOrElse(10))) // 提供limit的默认值为10,或者同时对page进行 -1 操作,以便在Slick中直接进行查询
case _ =>
None
}
}
val routes: Router.Routes = { // 使用方式,将整个查询字符串作为参数传入pagination
case GET(p"/posts/" ? pagination(p)) => Action {
Results.Ok(s"Posts: page = ${p.page} limit = ${p.limit}")
}
}
只需要写一次模板然后就可以重复使用,或者跟普通的解析器组合使用:
case GET(p"/posts/" ? pagination(p) & q"show_comments=${ bool(showComments) }") =>
将模板解析器与普通解析器使用符号 &
连接,或者将多个模板解析器组进行连接.
组合路由
如果我们在一个大型项目中用于上百个端点,这时路由文件会变得巨大而难以管理,这时候可以将路由拆开放在不同的模块,然后在一个类中组合.
比如有如下两个服务:
val service1 = Router.from {
case GET(p"hello/$to") => ???
}
val service2 = Router.from {
case GET(p"echo/$msg") => ???
}
Route 都是PartialFunctions(偏函数)类型,可以进行相互组合,使用Scala中简单的 orElse 方法:
Router.from(service1.routes orElse service2.routes)
这会生成一个更大的路由PartialFunctions,首先在第一个路由中尝试匹配,然后跳到第二个路由进行尝试匹配.Play同时提供了另一种特性, withPrefix:
service1.withPrefix("echo")
然后组合使用,匹配URL echo/hello/something
:
Router.from(service1.withPrefix("service").routes orElse service2.withPrefix("service2").routes)
或者使用更优雅的方式:
CompositeRouter(
"service1" -> service1.routes,
"service2" -> service2.routes
)
CompositeRouter的实现方式:
class CompositeRouter(routers: Seq[Router]) extends SimpleRouter {
override def documentation: Seq[(String, String, String)] =
routers.flatMap(_.documentation)
override def routes: Router.Routes =
routers.map(_.routes).fold(Router.empty.routes)(_ orElse _)
}
object CompositeRouter {
def fromPrefixedRoutes(routers: (String, Router.Routes)*): CompositeRouter =
new CompositeRouter(routers.map {
case (prefix, routes) =>
Router.from(routes).withPrefix(prefix)
})
}
Tamer M AbdulRadi: All you need to know about Play’s routing DSL