1. 概述
在 Scala 中开发简单的 CRUD 风格的 REST API,Play Framework 是一个非常不错的选择。它提供了简洁的 API,让我们无需编写大量冗余代码即可完成开发。
本文将带你一步步构建一个基于 Play Framework 的 REST API 服务,使用 JSON 作为数据格式,并涵盖常见的 HTTP 方法和状态码处理。
2. 示例项目
2.1. 我们将构建什么?
本例中,我们将实现一个简单的待办事项(Todo List)应用。为简化起见,我们不会连接数据库,而是将数据保存在内存中。
我们将逐步添加多个接口,并在过程中运行与测试我们的应用。
2.2. 项目初始化
首先,使用 sbt 模板 创建一个新的 Play Framework 项目:
$ sbt new playframework/play-scala-seed.g8
这会生成一个包含以下内容的项目:
- 一个控制器类(位于
app/controllers
目录) - 两个 HTML 页面(位于
app/views
目录) - 基础配置文件(位于
conf
目录)
由于我们不需要这些默认文件,可以删除以下内容:
HomeController.scala
index.scala.html
main.scala.html
同时清空 routes
文件中的内容。
3. 第一个 REST 接口
我们从最简单的接口开始:返回 NoContent
状态码。
3.1. 创建控制器
在 app/controllers
目录下新建一个控制器类:
@Singleton
class TodoListController @Inject()(val controllerComponents: ControllerComponents)
extends BaseController {
}
该类继承自 BaseController
,构造函数也兼容 Play 的依赖注入机制。
我们使用 @Inject
注解告诉 Play 自动注入依赖项,同时标记为 @Singleton
,确保整个应用生命周期中只有一个实例被创建。
3.2. 实现请求处理方法
接下来,在控制器中添加一个返回 NoContent
的方法:
def getAll(): Action[AnyContent] = Action {
NoContent
}
这个方法返回一个 Action
对象,代表一次 HTTP 请求处理逻辑。当前我们只是简单地返回了 NoContent
状态码。
3.3. 配置路由
在 routes
文件中添加一条路由规则:
GET /todo controllers.TodoListController.getAll
你需要指定:
- HTTP 方法(GET)
- 请求路径(/todo)
- 对应的控制器方法(全限定名)
为了便于阅读,建议按列对齐参数。
3.4. 启动并测试接口
启动应用:
$ sbt run
当看到如下日志时说明服务已就绪:
[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000
使用 curl 测试接口:
$ curl -v localhost:9000/todo
GET /todo HTTP/1.1
Host: localhost:9000
User-Agent: curl/7.64.1
Accept: */*
HTTP/1.1 204 No Content.
✅ 至此,我们已经成功搭建了一个基础的 Play 应用。
4. 返回所有待办事项
现在我们来扩展功能,让它能返回完整的待办列表。
4.1. 定义模型类
在 app/models
目录下创建模型类:
case class TodoListItem(id: Long, description: String, isItDone: Boolean)
4.2. 内存存储
在 TodoListController
中定义一个可变集合用于存储任务项:
class TodoListController @Inject()(val controllerComponents: ControllerComponents)
extends BaseController {
private val todoList = new mutable.ListBuffer[TodoListItem]()
todoList += TodoListItem(1, "test", true)
todoList += TodoListItem(2, "some other value", false)
为了方便调试,我们预设了两条初始数据。
4.3. JSON 格式化器
导入 Play 的 JSON 库:
import play.api.libs.json._
接着在控制器中定义格式化器:
implicit val todoListJson = Json.format[TodoListItem]
使用 implicit
可以避免每次调用 Json.toJson()
时都手动传入格式化器。
4.4. 修改 getAll 方法
修改 getAll
方法,使其根据列表是否为空返回不同响应:
def getAll(): Action[AnyContent] = Action {
if (todoList.isEmpty) {
NoContent
} else {
Ok(Json.toJson(todoList))
}
}
4.5. 测试接口
重新测试 GET 接口:
$ curl localhost:9000/todo
[
{
"id": 1,
"description": "test",
"isItDone": true
},
{
"id": 2,
"description": "some other value",
"isItDone": false
}
]
✅ 成功返回 JSON 格式的任务列表。
5. 查询单个任务
REST API 应支持通过路径参数获取单个资源,例如:
$ curl localhost:9000/todo/1
如果找不到对应 ID 的任务,应返回 NotFound
。
5.1. 添加路由规则
在 routes
文件中添加新的路由:
GET /todo/:itemId controllers.TodoListController.getById(itemId: Long)
:itemId
表示路径参数,Play 会自动将其解析为 Long
类型,并传给 getById
方法。
⚠️ 如果参数不是数字,则返回 BadRequest
。
5.2. 实现 getById 方法
在控制器中添加方法:
def getById(itemId: Long) = Action {
val foundItem = todoList.find(_.id == itemId)
foundItem match {
case Some(item) => Ok(Json.toJson(item))
case None => NotFound
}
}
使用 find
查找匹配项,结果为 Option[TodoListItem]
,通过模式匹配判断是否存在。
5.3. 测试接口
查找存在的任务:
$ curl localhost:9000/todo/1
{
"id": 1,
"description": "test",
"isItDone": true
}
查找不存在的任务:
$ curl -v localhost:9000/todo/999
HTTP/1.1 404 Not Found
✅ 接口行为符合预期。
6. 支持 PUT 和 DELETE 方法
Play Framework 支持所有 HTTP 方法,只需在 routes
文件中正确配置即可。
例如,我们可以添加两个接口:
PUT /todo/done/:itemId controllers.TodoListController.markAsDone(itemId: Long)
DELETE /todo/done controllers.TodoListController.deleteAllDone
这部分逻辑较为简单,此处略过具体实现。
7. 添加新任务
最后,我们需要支持通过 POST 请求添加新任务:
$ curl -v -d '{"description": "some new item"}' -H 'Content-Type: application/json' -X POST localhost:9000/todo
注意,客户端只需提供 description
字段,服务端会自动生成 id
和默认状态。
7.1. 添加路由规则
在 routes
文件中添加:
POST /todo controllers.TodoListController.addNewItem
7.2. 定义 DTO
在 app/models
目录下创建 DTO 类:
case class NewTodoListItem(description: String)
7.3. 解析 JSON 数据
在控制器中添加格式化器:
implicit val newTodoListJson = Json.format[NewTodoListItem]
然后实现解析逻辑:
def addNewItem() = Action { implicit request =>
val content = request.body
val jsonObject = content.asJson
val todoListItem: Option[NewTodoListItem] =
jsonObject.flatMap(
Json.fromJson[NewTodoListItem](_).asOpt
)
⚠️ 注意:必须设置 Content-Type: application/json
,否则 Play 会将请求体视为表单数据,导致解析失败。
7.4. 存储并返回结果
继续完善 addNewItem
方法:
todoListItem match {
case Some(newItem) =>
val nextId = todoList.map(_.id).max + 1
val toBeAdded = TodoListItem(nextId, newItem.description, false)
todoList += toBeAdded
Created(Json.toJson(toBeAdded))
case None =>
BadRequest
}
如果解析成功,就生成新的任务项并保存;否则返回 BadRequest
。
7.5. 测试接口
添加新任务:
$ curl -v -d '{"description": "some new item"}' -H 'Content-Type: application/json' -X POST localhost:9000/todo
HTTP/1.1 201 Created.
{
"id": 3,
"description": "some new item",
"isItDone": false
}
再次获取所有任务验证是否添加成功:
$ curl localhost:9000/todo
[
{
"id": 1,
"description": "test",
"isItDone": true
},
{
"id": 2,
"description": "some other value",
"isItDone": false
},
{
"id": 3,
"description": "some new item",
"isItDone": false
}
]
✅ 新任务成功添加到列表中。
8. 总结
本文我们使用 Scala 和 Play Framework 构建了一个简单的 REST API。
主要涵盖了:
- 项目初始化与路由配置
- 控制器和模型类的定义
- 使用 JSON 格式进行数据传输
- 使用 curl 进行接口测试
完整代码可参考 GitHub 仓库。