JSON with HTTP

    我们通过设计一个简单的、Restful 的 web 服务来说明一些必要的概念,通过 GET 来得到实体列表,POST 来创建新的实体。对于所有数据,该 web 服务使用的 Content-Type 均为 JSON。

    以下是用于我们服务的模型:

    首先,在控制器中导入必要的东西:

    1. import play.api.libs.json._
    2. import play.api.libs.functional.syntax._
    3. object Application extends Controller {
    4. }

    在写 Action 之前,我们先要处理模型到 JsValue 转换的问题,通过定义一个隐式的 Writes[Place] 即可。

    1. implicit val locationWrites: Writes[Location] = (
    2. (JsPath \ "lat").write[Double] and
    3. (JsPath \ "long").write[Double]
    4. )(unlift(Location.unapply))
    5. implicit val placeWrites: Writes[Place] = (
    6. (JsPath \ "name").write[String] and
    7. (JsPath \ "location").write[Location]
    8. )(unlift(Place.unapply))

    接着就可以写 Action 了:

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

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

    1. GET /places controllers.Application.listPlaces

    我们可以通过浏览器或 HTTP 工具来发送请求进行测试,下面我们通过 curl 进行测试:

    响应是:

    1. Content-Type: application/json; charset=utf-8
    2. Content-Length: 141
    3. [{"name":"Sandleford","location":{"lat":51.377797,"long":-1.318965}},{"name":"Watership Down","location":{"lat":51.235685,"long":-1.309197}}]

    对于接下来的 Action,我们需要定义一个隐式的 Reads[Place] 来将 JsValue 转换成我们的模型。

    1. implicit val locationReads: Reads[Location] = (
    2. (JsPath \ "lat").read[Double] and
    3. (JsPath \ "long").read[Double]
    4. )(Location.apply _)
    5. implicit val placeReads: Reads[Place] = (
    6. (JsPath \ "name").read[String] and
    7. (JsPath \ "location").read[Location]
    8. )(Place.apply _)

    然后,我们来定义这个 Action

    1. def savePlace = Action(BodyParsers.parse.json) { request =>
    2. val placeResult = request.body.validate[Place]
    3. placeResult.fold(
    4. errors => {
    5. BadRequest(Json.obj("status" ->"KO", "message" -> JsError.toFlatJson(errors)))
    6. },
    7. place => {
    8. Place.save(place)
    9. Ok(Json.obj("status" ->"OK", "message" -> ("Place '"+place.name+"' saved.") ))
    10. }
    11. )

    这个 Action 比前面那个要复杂,需要注意以下几点:

    • Action 接收的请求的 Content-Type 需要是 或 application/json,body 包含的是要创建的实体的 JSON 表示。
    • 它使用针对 JSON 的 BodyParser 来解析请求,并将 request.body 解析成 JsValue
    • 我们使用 validate 方法来做转换,它依赖于前面定义的隐式 Reads[Place]
    • 我们使用一个带有错误和成功处理的 fold 来处理 validate 的结果。这种模式也可以用于表单提交。
    • Action 发送的响应也是 JSON 格式的。
    1. POST /places controllers.Application.savePlace

    下面我们用有效及无效的请求来测试这个 action,以验证成功及错误处理的工作流。

    使用有效数据测试:

    响应:

    1. HTTP/1.1 200 OK
    2. Content-Type: application/json; charset=utf-8
    3. Content-Length: 57
    4. {"status":"OK","message":"Place 'Nuthanger Farm' saved."}

    使用无效数据测试(“name” 字段缺失):

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

    响应:

    1. HTTP/1.1 400 Bad Request
    2. Content-Type: application/json; charset=utf-8
    3. Content-Length: 79
    4. {"status":"KO","message":{"obj.name":[{"msg":"error.path.missing","args":[]}]}}

    使用无效数据测试(“lat” 数据类型错误):

    1. curl --include
    2. --request POST
    3. --header "Content-type: application/json"
    4. http://localhost:9000/places

    Play 天生支持 REST 和 JSON,因此开发此类服务应该是相当简单直观的。大部分的工作就是在为你的模型写 Reads 和 ,下一节我们将来详细介绍。