§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
比我们的列表案例更复杂。需要注意一些事项
- 此
Action
预期请求具有Content-Type
标头为text/json
或application/json
,并且主体包含要创建的实体的 JSON 表示。 - 它使用特定于 JSON 的
BodyParser
,它将 解析请求 并提供request.body
作为JsValue
。 - 我们使用
validate
方法进行转换,它将依赖于我们的隐式Reads[Place]
。 - 为了处理验证结果,我们使用了一个带有错误和成功流的
fold
。这种模式可能很熟悉,因为它也用于 表单提交。 Action
还发送 JSON 响应。
主体解析器可以使用案例类、显式 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,开发这些服务应该很直接。大部分工作都在编写 Reads
和 Writes
以用于您的模型,这将在下一节中详细介绍。
在此文档中发现错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?转到 我们的社区论坛 与社区开始对话。