Scala Web 与 HTTP 服务:Play、akka-http 与 http4s

系统对比 Scala 三大 Web 框架——Play Framework、akka-http、http4s:路由 DSL、JSON 处理(circe/play-json)、中间件、WebSocket、认证与生产部署,帮你按项目类型选型并落地。

引言

Scala 写 Web 服务有三条主流路线:Play Framework(全家桶、有状态)、akka-http(Akka 生态、低层灵活)、http4s(纯函数式、效果类型驱动)。三者都能扛生产流量,差别在抽象层级与生态哲学。本文逐个落地最小可用服务,再讲透 JSON、中间件、WebSocket 与部署,最后给选型矩阵。

前置:/scala-functional-programming/(效果类型基础)、/scala-functional-effects/(IO 生态)。数据库配合见 /scala-database-access/。


目录


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传统 MVCPlay session,HttpOnly + Secure
JWT / BearerAPI / 微服务签名校验、过期、密钥轮换
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 服务的集成测试与契约测试

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「scala」更多文章

  1. 纯函数式效果系统实战:Cats Effect IO 与 ZIO
  2. Scala.js 与 Scala Native:跨平台编译、互操作与工程实践
  3. Scala 领域建模实战:ADT、类型驱动设计与模块化架构