1. 简介
在本教程中,我们将探讨如何使用 Finch 来构建 REST API。
2. Finch 与 Finagle
Finch 是什么?根据其 GitHub 的介绍:
Finch 是一个轻量级的、纯函数式的抽象层,构建于强大的 Finagle 之上,用于构建可组合的 HTTP API。它的使命是为开发者提供简单且健壮的 HTTP 基础组件,尽可能贴近 Finagle 原生 API。
简单来说,Finch 是 Finagle 的一个纯函数式“前端”,而 Finagle 是 Twitter 开发的、用于构建高并发 JVM 服务的强大 RPC 系统。
Finagle 的亮点在于,它允许开发者使用统一的构建块来编写各种网络服务器和客户端(服务),无论它们使用的是哪种协议。而 Finch 则专注于构建 HTTP 服务。一个 HTTP 服务本质上是一对请求与响应:对于特定的请求,我们返回相应的响应。
Finch 和 Finagle 都由 Twitter 开发并维护。
3. 依赖配置
为了使用 Finch,我们需要添加以下依赖项:finch-core 和 finch-circe:
libraryDependencies ++= Seq(
"com.github.finagle" %% "finch-core" % "0.31.0",
"com.github.finagle" %% "finch-circe" % "0.31.0",
"io.circe" %% "circe-generic" % "0.9.0"
)
4. Hello, World API
我们从一个简单的 Hello, World! 示例开始:
object Main extends App {
val hello: Endpoint[String] = get("hello") { Ok("Hello, World!") }
Await.ready(Http.server.serve(":8081", hello.toService))
}
第 2 行中,我们定义了一个新的 Finch 接口。Finch 接口是对 HTTP 接口的抽象,与特定的 HTTP 方法绑定,接受请求并返回我们定义的响应。 后面我们会看到,多个接口可以非常方便地组合,从而构建出功能丰富的 API。
由于 Finch 是 Finagle 的纯函数式封装,底层仍然是 Finagle。Finagle 的 HTTP 服务只认识 Finagle 的 Service
类型,所以我们需要将 Finch 接口转换为 Finagle 服务。这正是第 4 行调用 .toService
所做的事情。
最后,我们将服务传入 serve
方法中,让服务器监听并处理请求。
执行 sbt run
后,访问 http://localhost:8081/hello:
$ curl localhost:8081/hello
Hello, World!
4.1. 接收 JSON 请求体
我们再添加一个接口,这次接收一个包含 first 和 last name 的 JSON 请求体,使用复合的 Finch 接口:
case class FullName(first: String, last: String)
val helloName: Endpoint[String] = post("hello" :: jsonBody[FullName]) { name: FullName =>
Ok(s"Hello, ${name.first} ${name.last}!")
}
然后修改服务器启动代码:
Await.ready(Http.server.serve(":8081", (hello :+: helloName).toService))
测试接口:
curl -X POST -H "Content-Type: application/json" -d '{"first":"John", "last":"Doe"}' localhost:8081/hello
"Hello, John Doe!"
如前所述,Finch 接口支持组合。上面使用了两个组合操作符:::
和 :+:
。
::
表示“然后”,用于串联路径和请求体。:+:
表示“或者”,用于提供接口的备选路径。
4.2. 关于 JSON 序列化和解析
Scala 中有多种 JSON 处理库。本教程使用的是 Circe,这是一个基于 Cats 和 Shapeless 构建的纯函数式库。
它的最大特点是:可以自动将 JSON 映射到 Scala case class,无需手动编写转换逻辑。
Finch 对 Circe 提供了开箱即用的支持,同时也支持其他库如 Argonaut、Jackson、JSON4s 等。
5. 完整的 REST API 示例
我们接下来构建一个基于 REST 原则的简单 todo 应用 API,数据存储使用 SQLite,数据库操作使用 Doobie(一个纯函数式、强类型的 JDBC 库)。
与之前不同的是,我们使用 Endpoint[IO, _]
来支持响应式编程风格。
5.1. 依赖配置
这次的依赖略有不同。我们使用 finchx-*
版本来支持多态接口,例如 Endpoint[IO, _]
:
libraryDependencies ++= Seq(
"com.github.finagle" %% "finchx-core" % "0.31.0",
"com.github.finagle" %% "finchx-circe" % "0.31.0",
"io.circe" %% "circe-generic" % "0.9.0",
"org.typelevel" %% "cats-effect" % "2.1.3",
"org.typelevel" %% "cats-core" % "2.1.1",
"org.xerial" % "sqlite-jdbc" % "3.31.1",
"org.tpolecat" %% "doobie-core" % "0.8.8",
)
5.2. 创建 Todo
我们使用 case class 定义 Todo
模型,Circe 会自动处理 JSON 转换:
case class Todo(
id: Option[Int],
name: String,
description: String,
done: Boolean
)
定义创建接口:
val createTodo: Endpoint[IO, Todo] = post(todosPath :: jsonBody[Todo]) { todo: Todo =>
for {
id <- sql"insert into todo (name, description, done) values (${todo.name}, ${todo.description}, ${todo.done})"
.update
.withUniqueGeneratedKeys[Int]("id")
.transact(xa)
created <- sql"select * from todo where id = $id"
.query[Todo]
.unique
.transact(xa)
} yield Created(created)
}
POST 请求测试:
$ curl -X POST -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": false}' \
localhost:8081/todos
{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}
5.3. 获取指定 Todo
val getTodo: Endpoint[IO, Todo] = get(todosPath :: path[Int]) { id: Int =>
for {
todos <- sql"select * from todo where id = $id"
.query[Todo]
.to[Set]
.transact(xa)
} yield todos.headOption match {
case None => NotFound(new Exception("Record not found"))
case Some(todo) => Ok(todo)
}
}
测试:
$ curl localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}
不存在时返回 404:
$ curl -i localhost:8081/todos/0
HTTP/1.1 404 Not Found
Date: Wed, 27 May 2020 00:33:28 GMT
Server: Finch
Content-Length: 0
5.4. 获取所有 Todo
val getTodos: Endpoint[IO, Seq[Todo]] = get(todosPath) {
for {
todos <- sql"select * from todo"
.query[Todo]
.to[Seq]
.transact(xa)
} yield Ok(todos)
}
测试:
$ curl localhost:8081/todos
[{"id":1,"name":"Hello, world","description":"From Baeldung","done":false},
{"id":2,"name":"Update Endpoint","description":"To be able to mark as completed","done":false},
{"id":3,"name":"Delete Todo Endpoint","description":"To be able to delete todos","done":false}]
5.5. 更新 Todo
val updateTodo: Endpoint[IO, Todo] = put(todosPath :: path[Int] :: jsonBody[Todo]) { (id: Int, todo: Todo) =>
for {
_ <- sql"update todo set name = ${todo.name}, description = ${todo.description}, done = ${todo.done} where id = $id"
.update
.run
.transact(xa)
todo <- sql"select * from todo where id = $id"
.query[Todo]
.unique
.transact(xa)
} yield Ok(todo)
}
测试更新:
$ curl -X PUT -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": true}' \
localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":true}
5.6. 删除 Todo
val deleteTodo: Endpoint[IO, Unit] = delete(todosPath :: path[Int]) { id: Int =>
for {
_ <- sql"delete from todo where id = $id"
.update
.run
.transact(xa)
} yield NoContent
}
测试删除:
$ curl -i -X DELETE localhost:8081/todos/3
HTTP/1.1 204 No Content
Date: Wed, 27 May 2020 01:05:08 GMT
Server: Finch
6. 总结
在本教程中,我们使用纯函数式编程的方式,通过 Finch 构建了 REST API,并展示了与 Doobie 的无缝集成。
如需更深入的使用细节,请参考 Finch 官方文档。
所有示例代码可在 GitHub 获取。