PlayFramework routing DSL

启动一个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