JSON 与 HTTP

通过HTTP API与JSON库的结合,Play 支持带Content-Type为JSON的HTTP请求和响应。

关于Controllers,、Actions和routing的详细介绍请查阅 HTTP 编程

我们通过设计一个简单的RESTful web服务来说明一些必要的概念,通过GET获取实体列表,接收POSTs来创建新实体。对于所有数据,服务都会使用JSON内容类型。

这里是我们的服务要使用的模型:

case class Location(lat: Double, long: Double)

case class Place(name: String, location: Location)

object Place {

  var list: List[Place] = {
    List(
      Place(
        "Sandleford",
        Location(51.377797, -1.318965)
      ),
      Place(
        "Watership Down",
        Location(51.235685, -1.309197)
      )
    )
  }

  def save(place: Place) = {
    list = list ::: List(place)
  }
}

以JSON格式提供实体列表

我们首先导入必要的东西到controller。

import play.api.mvc._
import play.api.libs.json._
import play.api.libs.functional.syntax._

object Application extends Controller {

}

在写Action之前, 我们需要建一个转换模型到JsValue 表示的管道。通过定义一个隐式Writes[Place]即可。

implicit val locationWrites: Writes[Location] = (
  (JsPath \ "lat").write[Double] and
  (JsPath \ "long").write[Double]
)(unlift(Location.unapply))

implicit val placeWrites: Writes[Place] = (
  (JsPath \ "name").write[String] and
  (JsPath \ "location").write[Location]
)(unlift(Place.unapply))

下一步我们写Action:

def listPlaces = Action {
  val json = Json.toJson(Place.list)
  Ok(json)
}

Action 接收一个Place 对象列表, 使用Json.toJson 和隐式Writes[Place] 转换他们到一个JsValue,然后作为result的body返回。Play会识别result为JSON,并为响应设置适当的Content-Type 标头和主体。

最后一步是在conf/routes文件中,为我们的Action 添加路由:

GET   /places               controllers.Application.listPlaces

我们可以通过浏览器或HTTP工具来发送请求进行测试。下面的例子通过使用unix命令行工具cURL

curl --include http://localhost:9000/places

响应:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 141

[{"name":"Sandleford","location":{"lat":51.377797,"long":-1.318965}},{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197}}]

从JSON格式创建新实体实例

为这个Action 我们需要定义一个隐式Reads[Place] 来转换JsValue 到模型。

implicit val locationReads: Reads[Location] = (
  (JsPath \ "lat").read[Double] and
  (JsPath \ "long").read[Double]
)(Location.apply _)

implicit val placeReads: Reads[Place] = (
  (JsPath \ "name").read[String] and
  (JsPath \ "location").read[Location]
)(Place.apply _)

下一步我们定义Action

def savePlace = Action(BodyParsers.parse.json) { request =>
  val placeResult = request.body.validate[Place]
  placeResult.fold(
    errors => {
      BadRequest(Json.obj("status" ->"KO", "message" -> JsError.toFlatJson(errors)))
    },
    place => { 
      Place.save(place)
      Ok(Json.obj("status" ->"OK", "message" -> ("Place '"+place.name+"' saved.") ))  
    }
  )
}

这个Action 比前面那个列表示例更复杂。需要注意以下几点:

  • Action 期望接收的请求的Content-Type 标头需要是text/jsonapplication/json ,并且body包含一个创建的实体的 JSON表示。
  • 它使用一个JSON 特定的BodyParser 解析请求和提供request.body 作为JsValue
  • 我们使用validate 方法来转换,它依赖于隐式Reads[Place]
  • 要处理验证结果, 我们使用带错误和成功处理的fold 。这种模式也类似于使用表单提交.
  • Action 发送的响应也是JSON格式。

最后我们在conf/routes文件中添加路由:

POST  /places               controllers.Application.savePlace

我们使用有效和无效的请求来测试这个action,验证成功及错误的工作流。

用有效数据测试action:

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : 51.244031,"long" : -1.263224}}' 
  http://localhost:9000/places

响应:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"status":"OK","message":"Place 'Nuthanger Farm' saved."}

用无效数据测试action, 丢失 “name” 字段:

curl --include
  --request POST
  --header "Content-type: application/json"
  --data '{"location":{"lat" : 51.244031,"long" : -1.263224}}' 
  http://localhost:9000/places

响应:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 79

{"status":"KO","message":{"obj.name":[{"msg":"error.path.missing","args":[]}]}}

用无效数据测试action, “lat”数据类型错误:

curl --include
  --request POST
  --header "Content-type: application/json" 
  --data '{"name":"Nuthanger Farm","location":{"lat" : "xxx","long" : -1.263224}}' 
  http://localhost:9000/places

响应:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 92

{"status":"KO","message":{"obj.location.lat":[{"msg":"error.expected.jsnumber","args":[]}]}}

小结

Play 天生设计为支持REST 和JSON,开发这些服务是相当简单直观的。大部分的工作就是为你的模型写ReadsWrites , 下一节我们详细介绍。