文档

§JSON 与 HTTP

Play 通过结合使用 HTTP API 和 JSON 库,支持内容类型为 JSON 的 HTTP 请求和响应。

有关控制器、操作和路由的详细信息,请参阅 HTTP 编程

我们将通过设计一个简单的 RESTful Web 服务来演示必要的概念,该服务用于获取实体列表并接受 POST 请求以创建新实体。该服务将对所有数据使用 JSON 内容类型。

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

case class Location(lat: Double, long: Double)
object Location {
  def unapply(l: Location): Option[(Double, Double)] = Some(l.lat, l.long)
}

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): Unit = {
    list = list ::: List(place)
  }

  def unapply(p: Place): Option[(String, Location)] = Some(p.name, p.location)
}

§以 JSON 格式提供实体列表

我们将从向控制器添加必要的导入开始。

import play.api.mvc._

class HomeController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {}

在编写 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 对象列表,使用我们的隐式 Writes[Place] 通过 Json.toJson 将它们转换为 JsValue,并将此作为结果主体返回。Play 将识别结果为 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
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[JsValue] = Action(parse.json) { request =>
  val placeResult = request.body.validate[Place]
  placeResult.fold(
    errors => {
      BadRequest(Json.obj("message" -> JsError.toJson(errors)))
    },
    place => {
      Place.save(place)
      Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
    }
  )
}

Action 比我们的列表案例更复杂。需要注意一些事项

主体解析器可以使用案例类、显式 Reads 对象或函数进行类型化。因此,我们可以将更多工作卸载到 Play 上,使其自动将 JSON 解析为案例类并 验证 它,甚至在调用我们的 Action 之前。

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

implicit val locationReads: Reads[Location] =
  (JsPath \ "lat")
    .read[Double](min(-90.0).keepAnd(max(90.0)))
    .and((JsPath \ "long").read[Double](min(-180.0).keepAnd(max(180.0))))(Location.apply _)

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

// This helper parses and validates JSON using the implicit `placeReads`
// above, returning errors if the parsed json fails validation.
def validateJson[A: Reads] = parse.json.validate(
  _.validate[A].asEither.left.map(e => BadRequest(JsError.toJson(e)))
)

// if we don't care about validation we could replace `validateJson[Place]`
// with `BodyParsers.parse.json[Place]` to get an unvalidated case class
// in `request.body` instead.
def savePlaceConcise: Action[Place] = Action(validateJson[Place]) { request =>
  // `request.body` contains a fully validated `Place` instance.
  val place = request.body
  Place.save(place)
  Ok(Json.obj("message" -> ("Place '" + place.name + "' saved.")))
}

最后,我们将在 conf/routes 中添加路由绑定。

POST  /places               controllers.Application.savePlace

我们将使用有效和无效请求测试此操作,以验证我们的成功和错误流。

使用有效数据测试操作

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
Content-Length: 57

{"message":"Place 'Nuthanger Farm' saved."}

使用无效数据测试操作,缺少“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
Content-Length: 79

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

使用无效数据测试操作,“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
Content-Length: 92

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

§总结

Play 旨在支持使用 JSON 的 REST,开发这些服务应该很直接。大部分工作都在编写 ReadsWrites 以用于您的模型,这将在下一节中详细介绍。

下一步:JSON Reads/Writes/Format 组合器


在此文档中发现错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?转到 我们的社区论坛 与社区开始对话。