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-corefinch-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 获取。


原始标题:Building REST APIs in Scala with Finch