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 仓库


原始标题:Building a REST API in Scala with Play Framework