引言
Scala 写 Web 服务有三条主流路线:Play Framework(全家桶、有状态)、akka-http(Akka 生态、低层灵活)、http4s(纯函数式、效果类型驱动)。三者都能扛生产流量,差别在抽象层级与生态哲学。本文逐个落地最小可用服务,再讲透 JSON、中间件、WebSocket 与部署,最后给选型矩阵。
前置:/scala-functional-programming/(效果类型基础)、/scala-functional-effects/(IO 生态)。数据库配合见 /scala-database-access/。
目录
- 1. Scala Web 生态全景
- 2. Play Framework:路由与控制器
- 3. akka-http:低层路由 DSL
- 4. http4s:纯函数式 HTTP
- 5. JSON 处理:circe 与 play-json
- 6. 中间件与错误处理
- 7. WebSocket 与 SSE
- 8. 认证与安全
- 9. 生产部署
- 10. 框架选型速查表
- 延伸阅读
1. Scala Web 生态全景
| 框架 | 抽象层级 | 状态管理 | 生态 | 学习曲线 |
|---|---|---|---|---|
| Play | 高(全家桶) | 有状态(默认) | 模板、ORM、插件全 | 平缓 |
| akka-http | 中低 | 无(低层) | Akka 流、集群 | 中等 |
| http4s | 高(纯函数式) | 无(IO 驱动) | Cats Effect / ZIO | 陡(函数式前置) |
一句话选型:
- 团队熟悉 MVC / 需要内置模板 → Play
- 已在 Akka 集群上做服务 → akka-http
- 追求纯函数、类型安全、可测试 → http4s
三者都跑在 Netty/Jetty 之上,QPS 差距不大——选型看生态与团队,不看裸性能。
2. Play Framework:路由与控制器
Play 是「开箱即用」的路由式 MVC,conf/routes 声明路由:
# conf/routes
GET /users controllers.UserController.list
POST /users controllers.UserController.create
GET /users/:id controllers.UserController.show(id: Long)
PUT /users/:id controllers.UserController.update(id: Long)
控制器返回 Action:
package controllers
import play.api.mvc.{BaseController, ControllerComponents}
import play.api.libs.json.Json
import javax.inject.{Inject, Singleton}
@Singleton
class UserController @Inject() (val controllerComponents: ControllerComponents)
extends BaseController {
def list = Action {
Ok(Json.obj("users" -> Seq("Alice", "Bob")))
}
def show(id: Long) = Action {
if (id > 0) Ok(s"user $id")
else BadRequest("invalid id")
}
}
Play 特色:依赖注入(Guice)、Twirl 模板、内置 WS 客户端、开发模式热重载——适合「传统 Web 应用 + 后台系统」。
3. akka-http:低层路由 DSL
akka-http 把 HTTP 当流处理,路由用 DSL 组合子,小而清晰:
// build.sbt
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.5.3"
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.6.21"
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives._
implicit val system: ActorSystem = ActorSystem("http")
val route =
pathPrefix("users") {
concat(
pathEndOrSingleSlash {
get { complete(Seq("Alice", "Bob")) } // GET /users
},
path(LongNumber) { id => // GET /users/:id
get { complete(s"user $id") }
},
post { // POST /users
entity(as[String]) { body => complete(201, body) }
}
)
}
Http().newServerAt("0.0.0.0", 8080).bind(route)
Directives 核心组合子:path/pathPrefix/get/post/complete/entity/reject——全部是可组合函数,自由嵌套表达任意 URL 形状。
4. http4s:纯函数式 HTTP
http4s 把「HTTP 请求→响应」建模成纯函数 Request[F] => F[Response[F]],与 Cats Effect 无缝:
// build.sbt
libraryDependencies += "org.http4s" %% "http4s-dsl" % "1.0.0-M41"
libraryDependencies += "org.http4s" %% "http4s-ember-server" % "1.0.0-M41"
import cats.effect.{IO, IOApp}
import org.http4s.{HttpApp, HttpRoutes, Response, Status}
import org.http4s.dsl.io._
import org.http4s.ember.server.EmberServerBuilder
object Main extends IOApp.Simple {
val routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "users" =>
Ok(Seq("Alice", "Bob"))
case GET -> Root / "users" / LongVar(id) =>
if (id > 0) Ok(s"user $id") else BadRequest("invalid")
case req @ POST -> Root / "users" =>
req.as[String].flatMap(body => Created(body))
}
val app: HttpApp[IO] = routes.orNotFound
val run: IO[Unit] =
EmberServerBuilder.default[IO]
.withHost("0.0.0.0")
.withPort(8080)
.withHttpApp(app)
.build
.useForever
}
http4s 价值:所有副作用都在 F 里显式管理、测试可替换、中间件即普通函数——最「Scala 正统」的 Web 写法。
5. JSON 处理:circe 与 play-json
circe(配合 http4s / akka-http,基于类型类):
// build.sbt
libraryDependencies += "io.circe" %% "circe-core" % "0.14.9"
libraryDependencies += "io.circe" %% "circe-generic" % "0.14.9"
libraryDependencies += "io.circe" %% "circe-parser" % "0.14.9"
import io.circe._
import io.circe.generic.auto._
import io.circe.syntax._
case class User(id: Long, name: String, active: Boolean = true)
val user = User(1L, "Alice")
val json: Json = user.asJson
// {"id":1,"name":"Alice","active":true}
val decoded: Either[Error, User] = json.as[User]
play-json(配合 Play):
import play.api.libs.json.{Json, Reads, Writes, OWrites}
implicit val userWrites: OWrites[User] = Json.writes[User]
implicit val userReads: Reads[User] = Json.reads[User]
val js = Json.toJson(user) // 序列化
val back = js.as[User] // 反序列化
JSON 技巧表:
| 场景 | 写法 |
|---|---|
| 忽略字段 | @JsonIgnore / 自定义 Decoder |
| 蛇形 vs 驼峰 | 自定义 SnakeCase 命名策略 |
| 可选字段 | Option[T] 自动处理 null/缺失 |
| 时间类型 | 自定义 Encoder[Instant] 用 ISO 8601 |
| 失败处理 | Either[Error, T] / orElse 默认值 |
6. 中间件与错误处理
akka-http 中间件:包一层 Route => Route:
def logging(inner: Route): Route = extractRequest { req =>
println(s"→ ${req.method} ${req.uri}")
inner
}
def withErrorHandling(inner: Route): Route = {
handleExceptions(ExceptionHandler {
case e: IllegalArgumentException =>
complete(StatusCodes.BadRequest, e.getMessage)
case _ =>
complete(StatusCodes.InternalServerError, "server error")
}) {
handleRejections(RejectionHandler.default) { inner }
}
}
val app = withErrorHandling(logging(route))
http4s 中间件(函数组合):
def logging(service: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli { req =>
service.run(req).map(resp => {
println(s"→ ${req.method} ${req.uri} = ${resp.status}")
resp
})
}
val app = logging(routes).orNotFound
统一错误响应约定(跨框架一致):
{"error": {"code": 400, "message": "invalid id", "traceId": "abc-123"}}
traceId 贯穿请求是微服务排查的命根子,参考 [[observability]] 全链路专题。
7. WebSocket 与 SSE
akka-http WebSocket:
import akka.http.scaladsl.server.Directives._
import akka.stream.scaladsl.{Flow, Source}
val wsFlow: Flow[Message, Message, Any] =
Flow[Message].collect { case TextMessage.Strict(t) => TextMessage(s"echo: $t") }
val route =
path("ws") {
handleWebSocketMessages(wsFlow)
}
http4s SSE(Server-Sent Events):
import fs2.Stream
import org.http4s.syntax.literals._
val events: Stream[IO, Event] =
Stream.eval(IO(Event("tick")))
.repeat
.metered(1.second)
val route: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "events" =>
Ok(events)
case GET -> Root / "ws" =>
// 需要 scala-js / 客户端配合,此处示意
BadRequest("use SSE for unidirectional")
}
选型:双向实时 → WebSocket;单向推送(行情/通知)→ SSE(HTTP 兼容、自动重连更简单)。
8. 认证与安全
常见认证方案:
| 方案 | 适用 | 实现要点 |
|---|---|---|
| Session + Cookie | 传统 MVC | Play session,HttpOnly + Secure |
| JWT / Bearer | API / 微服务 | 签名校验、过期、密钥轮换 |
| OAuth2 委托 | 第三方登录 | 授权码流程、PKCE |
| API Key | 服务到服务 | 头校验 + 速率限制 |
JWT 校验(http4s 中间件示意):
val protectedRoutes: HttpRoutes[IO] = HttpRoutes.of[IO] {
case GET -> Root / "me" =>
Ok("protected data")
}
val withAuth: HttpRoutes[IO] = Kleisli { req =>
req.headers.get(ci"Authorization") match {
case Some(header) if verifyJwt(header.value) =>
protectedRoutes.run(req)
case _ =>
Response[IO](Status.Unauthorized).pure[IO]
}
}
安全清单:HTTPS 终止、限制 Body 大小、防路径穿越、CORS 白名单、日志脱敏(令牌/密码)、依赖漏洞扫描。
9. 生产部署
sbt 打包(Play 自带 sbt dist,akka-http/http4s 用 sbt-native-packager):
// plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")
// build.sbt
enablePlugins(JavaAppPackaging)
sbt stage # 生成 target/universal/stage/bin/<app>
Docker 部署:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/universal/stage /app
EXPOSE 8080
CMD ["/app/bin/myapp"]
生产关注点:
| 维度 | 实践 |
|---|---|
| 配置 | 环境变量注入、多环境 profile |
| 日志 | structured logging(JSON)、traceId 贯穿 |
| 健康检查 | /health 探针(内存、依赖状态) |
| 优雅停机 | JVM shutdown hook 排空连接 |
| 扩展 | 无状态 → 横向扩容 + 负载均衡 |
10. 框架选型速查表
| 需求 | 推荐 |
|---|---|
| 传统 MVC + 模板后台 | Play |
| Akka 集群内的流式服务 | akka-http |
| 纯函数式 + 类型安全 + 可测试 | http4s |
| JSON 序列化 | circe(类型类)/ play-json |
| 双向实时 | WebSocket(akka-http / http4s) |
| 单向推送 | SSE |
| API 认证 | JWT 中间件 |
| 打包部署 | sbt-native-packager + Docker |
一句话记忆:MVC 后台选 Play,Akka 流上选 akka-http,纯函数信仰选 http4s——JSON 全用 circe,部署统一 Docker。
延伸阅读
- /scala-functional-effects/ — http4s 依赖的 Cats Effect IO 生态
- /scala-database-access/ — Web 服务的持久层:Slick / doobie
- /scala-build-tooling/ — sbt 多模块、打包与 CI
- /scala-testing-practice/ — HTTP 服务的集成测试与契约测试
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。