Play Framework

Java/Scala対応のリアクティブWebフレームワーク。ホットリロードと非同期プログラミングによる高い開発生産性が特徴。

ScalaJavaフレームワークWeb開発Reactive非同期

GitHub概要

playframework/playframework

The Community Maintained High Velocity Web Framework For Java and Scala.

スター12,580
ウォッチ618
フォーク4,086
作成日:2011年9月7日
言語:Scala
ライセンス:Apache License 2.0

トピックス

frameworkhacktoberfestjavajvmplayplayframeworkreactiverestfulscalaweb-frameworkwebapps

スター履歴

playframework/playframework Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Play Framework

概要

Play Frameworkは、ScalaとJavaで高生産性なWebアプリケーションを開発するためのモダンなフレームワークです。

詳細

Play Framework(プレイフレームワーク)は、Lightbend(旧Typesafe)によって開発されたオープンソースのWebアプリケーションフレームワークです。2011年に最初のリリースが行われ、Ruby on RailsやDjangoに影響を受けながらも、JVM上での高パフォーマンスと関数型プログラミングの利点を活かした設計となっています。リアクティブアーキテクチャを採用し、ノンブロッキングI/Oによる高い並行性能を実現します。Akkaアクターシステムをベースとした非同期処理により、大量のリクエストを効率的に処理可能です。開発者体験を重視した設計で、ホットリロード機能により変更が即座に反映され、高速な開発サイクルを実現します。型安全なルーティング、テンプレートエンジン、JSONサポート、RESTful APIの構築支援など、モダンWeb開発に必要な機能を包括的に提供しています。ScalaとJavaの両方をサポートし、既存のJavaエコシステムとの親和性も高く、エンタープライズアプリケーション開発に適しています。

メリット・デメリット

メリット

  • リアクティブアーキテクチャ: 高い並行性能とスケーラビリティ
  • ホットリロード: 開発中の変更が即座に反映される
  • 型安全性: コンパイル時のエラー検出とIDEサポート
  • 非同期処理: ノンブロッキングI/Oによる高性能
  • ScalaとJava両対応: 開発者のスキルセットに応じた選択
  • 豊富なプラグイン: 認証、データベース、キャッシュなどの拡張機能
  • RESTfulサポート: JSON APIの構築が容易
  • エンタープライズ対応: 大規模システムでの実績

デメリット

  • 学習コスト: Scala特有の概念と関数型プログラミングの理解が必要
  • 複雑な設定: 大規模プロジェクトでの設定管理の複雑さ
  • メモリ消費: JVMベースのため初期メモリ使用量が大きい
  • デバッグの難しさ: 非同期処理とリアクティブストリームのデバッグ
  • 限定的なコミュニティ: RailsやExpressと比較して開発者コミュニティが小さい
  • JVM起動時間: 開発環境での初回起動に時間がかかる

主要リンク

書き方の例

Hello World

// app/controllers/HomeController.scala
package controllers

import javax.inject._
import play.api._
import play.api.mvc._

@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {

  def index() = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.index())
  }

  def hello(name: String) = Action {
    Ok(s"Hello $name!")
  }
}
<!-- app/views/index.scala.html -->
@()

@main("Welcome to Play") {
  <h1>Welcome to Play!</h1>
  <p>Hello World from Play Framework</p>
}

ルーティング

# conf/routes
# Routes
# This file defines all application routes (Higher priority routes first)

# Home page
GET     /                           controllers.HomeController.index

# Hello endpoint
GET     /hello/:name                controllers.HomeController.hello(name: String)

# Users API
GET     /api/users                  controllers.UserController.list
POST    /api/users                  controllers.UserController.create
GET     /api/users/:id              controllers.UserController.show(id: Long)
PUT     /api/users/:id              controllers.UserController.update(id: Long)
DELETE  /api/users/:id              controllers.UserController.delete(id: Long)

# Static assets
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

JSON APIの作成

// app/controllers/UserController.scala
package controllers

import javax.inject._
import play.api.mvc._
import play.api.libs.json._
import models.User
import services.UserService
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class UserController @Inject()(
  val controllerComponents: ControllerComponents,
  userService: UserService
)(implicit ec: ExecutionContext) extends BaseController {

  implicit val userFormat: Format[User] = Json.format[User]

  def list = Action.async {
    userService.findAll().map { users =>
      Ok(Json.toJson(users))
    }
  }

  def show(id: Long) = Action.async {
    userService.findById(id).map {
      case Some(user) => Ok(Json.toJson(user))
      case None => NotFound(Json.obj("error" -> "User not found"))
    }
  }

  def create = Action.async(parse.json) { request =>
    request.body.validate[User] match {
      case JsSuccess(user, _) =>
        userService.create(user).map { createdUser =>
          Created(Json.toJson(createdUser))
        }
      case JsError(errors) =>
        Future.successful(BadRequest(Json.obj("errors" -> JsError.toJson(errors))))
    }
  }
}

データベース操作(Slick)

// app/models/User.scala
package models

import slick.jdbc.PostgresProfile.api._
import java.time.LocalDateTime

case class User(
  id: Option[Long] = None,
  name: String,
  email: String,
  createdAt: Option[LocalDateTime] = None
)

class UserTable(tag: Tag) extends Table[User](tag, "users") {
  def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def email = column[String]("email")
  def createdAt = column[LocalDateTime]("created_at")

  def * = (id.?, name, email, createdAt.?) <> ((User.apply _).tupled, User.unapply)
}

object Users {
  val table = TableQuery[UserTable]
}
// app/services/UserService.scala
package services

import javax.inject._
import models.{User, Users}
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import slick.jdbc.JdbcProfile
import scala.concurrent.{ExecutionContext, Future}
import java.time.LocalDateTime

@Singleton
class UserService @Inject()(
  protected val dbConfigProvider: DatabaseConfigProvider
)(implicit ec: ExecutionContext) extends HasDatabaseConfigProvider[JdbcProfile] {

  import profile.api._

  def findAll(): Future[Seq[User]] = {
    db.run(Users.table.result)
  }

  def findById(id: Long): Future[Option[User]] = {
    db.run(Users.table.filter(_.id === id).result.headOption)
  }

  def create(user: User): Future[User] = {
    val userWithTimestamp = user.copy(createdAt = Some(LocalDateTime.now()))
    val insertQuery = Users.table returning Users.table.map(_.id) into ((user, id) => user.copy(id = Some(id)))
    db.run(insertQuery += userWithTimestamp)
  }

  def update(id: Long, user: User): Future[Option[User]] = {
    val updateQuery = Users.table.filter(_.id === id).update(user.copy(id = Some(id)))
    db.run(updateQuery).flatMap {
      case 0 => Future.successful(None)
      case _ => findById(id)
    }
  }

  def delete(id: Long): Future[Boolean] = {
    db.run(Users.table.filter(_.id === id).delete).map(_ > 0)
  }
}

非同期処理とフューチャー

// app/controllers/AsyncController.scala
package controllers

import javax.inject._
import play.api.mvc._
import play.api.libs.ws._
import play.api.libs.json._
import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._

@Singleton
class AsyncController @Inject()(
  val controllerComponents: ControllerComponents,
  ws: WSClient
)(implicit ec: ExecutionContext) extends BaseController {

  def fetchData = Action.async {
    // 複数のAPIを並行して呼び出し
    val futureResult1 = ws.url("https://api.example1.com/data").get()
    val futureResult2 = ws.url("https://api.example2.com/data").get()
    
    for {
      result1 <- futureResult1
      result2 <- futureResult2
    } yield {
      val combinedData = Json.obj(
        "api1" -> result1.json,
        "api2" -> result2.json
      )
      Ok(combinedData)
    }
  }

  def processWithTimeout = Action.async {
    val futureResult = processLongRunningTask()
    
    // タイムアウト設定
    val timeoutFuture = play.api.libs.concurrent.Futures.timeout(10.seconds)
    
    Future.firstCompletedOf(Seq(futureResult, timeoutFuture)).map {
      case result: String => Ok(result)
      case _ => RequestTimeout("Processing timeout")
    }
  }

  private def processLongRunningTask(): Future[String] = {
    Future {
      Thread.sleep(5000) // 重い処理のシミュレーション
      "Task completed"
    }
  }
}

フォーム処理とバリデーション

// app/controllers/FormController.scala
package controllers

import javax.inject._
import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._
import models.User
import scala.concurrent.{ExecutionContext, Future}

case class UserForm(name: String, email: String, age: Int)

@Singleton
class FormController @Inject()(
  val controllerComponents: ControllerComponents
)(implicit ec: ExecutionContext) extends BaseController {

  val userForm = Form(
    mapping(
      "name" -> nonEmptyText(minLength = 2, maxLength = 50),
      "email" -> email,
      "age" -> number(min = 0, max = 120)
    )(UserForm.apply)(UserForm.unapply)
  )

  def showForm = Action { implicit request =>
    Ok(views.html.userForm(userForm))
  }

  def submitForm = Action.async { implicit request =>
    userForm.bindFromRequest().fold(
      formWithErrors => {
        Future.successful(BadRequest(views.html.userForm(formWithErrors)))
      },
      userData => {
        // フォームデータの処理
        processUserData(userData).map { success =>
          if (success) {
            Redirect(routes.FormController.showForm())
              .flashing("success" -> "User created successfully")
          } else {
            BadRequest(views.html.userForm(userForm.withGlobalError("Failed to create user")))
          }
        }
      }
    )
  }

  private def processUserData(userData: UserForm): Future[Boolean] = {
    // データベースへの保存処理
    Future.successful(true)
  }
}

テスト

// test/controllers/HomeControllerSpec.scala
package controllers

import org.scalatestplus.play._
import org.scalatestplus.play.guice._
import play.api.test._
import play.api.test.Helpers._
import play.api.libs.json._

class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting {

  "HomeController GET" should {

    "render the index page from the application" in {
      val controller = inject[HomeController]
      val home = controller.index().apply(FakeRequest(GET, "/"))

      status(home) mustBe OK
      contentType(home) mustBe Some("text/html")
      contentAsString(home) must include("Welcome to Play")
    }

    "render the hello page" in {
      val controller = inject[HomeController]
      val hello = controller.hello("Bob").apply(FakeRequest(GET, "/hello/Bob"))

      status(hello) mustBe OK
      contentType(hello) mustBe Some("text/plain")
      contentAsString(hello) mustBe "Hello Bob!"
    }
  }

  "UserController" should {

    "return JSON user list" in {
      val request = FakeRequest(GET, "/api/users")
      val result = route(app, request).get

      status(result) mustBe OK
      contentType(result) mustBe Some("application/json")
      
      val json = contentAsJson(result)
      json mustBe a[JsArray]
    }
  }
}