Play 2.6のドキュメントにある「What’s new in Play 2.6」からScalaに関する部分をざっと日本語にしてみました。
細かい部分はマイグレーションガイドも見ないとダメそうですね。こちらはかなり分量があるのでさくっと日本語にするのは厳しそう…。
グローバルステートが非推奨に
アプリケーションではplay.api.Play.current
/ play.Play.application()
でグローバルアプリケーションにアクセスできるけど非推奨。以下のようにして禁止することもできる。
play.allowGlobalApplication=false
こうしておくとPlay.current
が例外をスローするようになる。
Akka HTTPベースのバックエンド
Akka HTTPベースのバックエンドがデフォルトになった。Nettyのバックエンドも利用可能。Nettyを使用する場合は以下を参照:
HTTP/2サポート (Experimental)
PlayAkkaHttp2Support
モジュールを使うことでHTTP/2サポートを利用可能。
lazy val root = (project in file("."))
.enablePlugins(PlayJava, PlayAkkaHttp2Support)
デフォルトではrun
コマンドでは動作しない。詳細は以下を参照:
リクエストの属性
リクエストが属性を含むようになった。属性にリクエストに関する追加の情報を格納することができる。たとえばフィルタで値をセットしておき、後続のアクションでその値を参照するといったことができる。
属性はTypedMap
というタイプセーフかつイミュータブルなマップに格納される。
object Attrs {
val User: TypedKey[User] = TypedKey[User].apply("user")
}
val user: User = req.attrs(Attrs.User)
val newReq = req.addAttr(Attrs.User, newUser)
これに伴ってリクエストタグは非推奨になった。リクエストタグはリクエスト属性に置き換えることが望ましい。
Route modifierタグ
routeファイルに、routeごとのカスタム動作を指定するための"modifiers"を記述できるようになった。たとえば"nocsrf"というタグを記述しておくとCSRFフィルタが適用されなくなる。
+ nocsrf # Don't CSRF protect this route
POST /api/foo/bar ApiController.foobar
独自のmodifierを作成することもでき、+
のあとに空白で区切っていくつでもタグを記述することができる。
modifierタグはHandlerDef
リクエスト属性で参照可能(これはroutesファイルで定義された他のメタデータも含んでいる)。
import play.api.routing.{ HandlerDef, Router }
import play.api.mvc.RequestHeader
val handler = request.attrs(Router.Attrs.HandlerDef)
val modifiers = handler.modifiers
TwirlテンプレートでのDI
Twirlテンプレートで@this
と記述するとコンストラクタを作成できるようになった。これによってコントローラ経由ではなくテンプレートに直接DIが可能になった。
たとえば以下のテンプレートはコントローラでは使用しないTemplateRenderingComponent
というコンポーネントをDIしている。@this
は@()
より前に記述する必要がある点に注意。
@this(trc: TemplateRenderingComponent)
@(item: Item)
@{trc.render(item)}
デフォルトではすべての@this
が記述されたテンプレートクラスには自動的に@javax.inject.Inject()
が付与される。この挙動はbuild.sbt
で変更できる。
TwirlKeys.constructorAnnotations += "@java.lang.Deprecated()"
TwirlKeys.constructorAnnotations := Seq("@com.google.inject.Inject()")
コントローラではテンプレートクラスをDIして使用する。
public MyController @Inject()(indexTemplate: views.html.IndexTemplate,
cc: ControllerComponents)
extends AbstractController(cc) {
def index = Action { implicit request =>
Ok(indexTemplate())
}
}
フィルタの改善
以下のフィルタはデフォルトで有効。
play.filters.csrf.CSRFFilter
play.filters.headers.SecurityHeadersFilter
play.filters.hosts.AllowedHostsFilter
application.conf
では+=
でフィルタを追加することができる。
play.filters.enabled+=MyFilter
テストなどの目的でフィルタを無効にしたい場合はこんな感じで。
play.filters.disabled+=MyFilter
詳細は以下を参照:
NOTE: CSRF.formField
のようなCSRFフォームヘルパーを使用していない既存のプロジェクトはおそらくPUTおよびPOSTリクエストが"403 Forbidden"になるはず。この挙動をチェックするには<logger name="play.filters.csrf" value="TRACE"/>
をlogback.xml
に追加する。また、もしlocalhost以外でPlayアプリケーションを実行している場合、AllowedHostsFilterでアクセス元のホスト名/IPアドレスを設定する必要がある。
gzipフィルタを使っている場合、独自のフィルタを書く代わりにapplication.conf
で適用するContentTypeを指定できるようになった。
play.filters.gzip {
contentType {
# If non empty, then a response will only be compressed if its content type is in this list.
whiteList = [ "text/*", "application/javascript", "application/json" ]
# The black list is only used if the white list is empty.
# Compress all responses except the ones whose content type is in this list.
blackList = []
}
}
JWTクッキー
セッションとFlashのクッキーにデフォルトでJSON Web Tokenを使用するようになった。
詳細は以下を参照:
Logging Marker API
play.Logger
とplay.api.Logger
でSLF4Jのマーカーがサポートされた。
import play.api._
logger.info("some info message")(MarkerContext(someMarker))
これによってマーカーを暗黙的に渡すことが可能になる。たとえばLogstash Logback Encoderを使う場合、こんなトレイトを定義しておき、
trait RequestMarkerContext {
implicit def requestHeaderToMarkerContext(request: RequestHeader): MarkerContext = {
import net.logstash.logback.marker.LogstashMarker
import net.logstash.logback.marker.Markers._
val requestMarkers: LogstashMarker = append("host", request.host)
.and(append("path", request.path))
MarkerContext(requestMarkers)
}
}
コントローラで使用すると、以下のような感じで異なるExecutionContextを使用するFuture
にも持ち回ることができる。
def asyncIndex = Action.async { implicit request =>
Future {
methodInOtherExecutionContext()
}(otherExecutionContext)
}
def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
logger.debug("index: ")
Ok("testing")
}
マーカーコンテキストは"tracer bullet"スタイルのロギング(明示的にログレベルを変更せずに特定のリクエストのログを出力したい場合)に便利。たとえば特定の条件の場合のみマーカーを追加することができる。
trait TracerMarker {
import TracerMarker._
implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic")
if (request.getQueryString("trace").nonEmpty) {
marker.add(tracerMarker)
}
marker
}
}
object TracerMarker {
private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}
class TracerBulletController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) with TracerMarker {
private val logger = play.api.Logger("application")
def index = Action { implicit request: Request[AnyContent] =>
logger.trace("Only logged if queryString contains trace=true")
Ok("hello world")
}
}
logback.xml
で以下のTurboFilterでログをトリガーできる。
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Name>TRACER_FILTER</Name>
<Marker>TRACER</Marker>
<OnMatch>ACCEPT</OnMatch>
</turboFilter>
詳細は以下を参照。
また、マーカーを使ったロギングについては以下を参照:
Configurationの改善
play.api.Configuration
にカスタム型で読み取るための新しいメソッドが追加された。任意の型に対応したimplicitなConfigLoader
を使用することができる。詳細は以下を参照:
セキュリティログ
Playのセキュリティに関するオペレーションにはsecurityマーカーが追加され、セキュリティチェックに失敗するとWARNレベルでログが出力されるようになった。これによって開発者がなぜリクエストが失敗したのかを把握できるようになった。Play 2.6ではセキュリティフィルタがデフォルトで有効になったため、これは重要なことである。
securityマーカーを通常のログと分けてトリガーしたりフィルタすることができる。たとえばsecurityマーカーがセットされているログを無効にするにはlogback.xml
に以下の設定を追加する。
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>SECURITY</Marker>
<OnMatch>DENY</OnMatch>
</turboFilter>
PlayではMessagesApi
とLang
クラスが国際化のために使用されており、フォームでのエラーメッセージの表示にも必要だったが、これまではいろいろ面倒なステップが必要だった。Play 2.6ではI18N APIがリファインされた。
MessagesActionBuilder
が追加された。これはMessagesRequest
(MessageProvider
を継承したWrappedRequest
)を提供する。テンプレートには1つのimplicitパラメータを定義するだけでよく、コントローラはI18nSupport
を継承する必要はない。
class FormController @Inject()(messagesAction: MessagesActionBuilder, components: ControllerComponents)
extends AbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
def index = messagesAction { implicit request: MessagesRequest[AnyContent] =>
Ok(views.html.displayForm(userForm))
}
def post = ...
}
この場合のdisplayForm.scala.html
はこんな感じ。
@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)
@import helper._
@helper.form(action = routes.FormController.post()) {
@CSRF.formField @* <- takes a RequestHeader *@
@helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
@helper.inputText(userForm("age")) @* <- takes a MessagesProvider *@
}
詳細以下を参照:
テストサポート
MessagesApi
のインスタンスの作成も改善されており、DefaultMessagesApi()
やDefaultLangs()
をデフォルト引数で生成できるようになった。メッセージを指定したい場合は以下のようにできる。
val messagesApi: MessagesApi = {
val env = new Environment(new File("."), this.getClass.getClassLoader, Mode.Dev)
val config = Configuration.reference ++ Configuration.from(Map("play.i18n.langs" -> Seq("en", "fr", "fr-CH")))
val langs = new DefaultLangsProvider(config).get
new DefaultMessagesApi(testMessages, langs)
}
Futureのタイムアウトとディレイのサポート
play.api.libs.concurrent.Futures
トレイトを使用する。
import play.api.libs.concurrent.Futures._
class MyController @Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) {
def index = Action.async {
intensiveComputation().withTimeout(1.seconds).map { i =>
Ok("Got result: " + i)
}.recover {
case e: TimeoutException =>
InternalServerError("timeout")
}
}
}
同様に指定した時間後にFutureを実行するdelayed
メソッドも利用可能。詳細は以下を参照:
CustomExecutionContextとスレッドプールサイズ
CustomExecutionContext
はakka.actor.ActorSystem
にデリゲートするカスタムExecutionContextを定義する。これはデフォルトのExecutionContextを使用するべきではない状況(たとえばデータベースアクセスやブロッキングI/Oなど)で便利。
Playのテンプレートプロジェクトを更新
PlayのダウンロードページにあるブロッキングAPI(AnormやJPAなど)を使用するサンプルテンプレートはカスタムExecutionContextを使うように変更された。たとえばJPAのサンプルではDatabaseExecutionContext
を使用するようになっている。
JDBCのコネクションプールに関わるスレッドプールは、ThreadPoolExecutorを使用してコネクションプールのサイズにあわせて固定したいはず。HikariCPではコネクションプールのサイズは物理コア数の2倍+ディスクのスピンドル数に設定するべきとされている。dispatcherの設定はこんな感じ。
# db connections = ((physical_core_count * 2) + effective_spindle_count)
fixedConnectionPool = 9
database.dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = ${fixedConnectionPool}
}
}
CustomExecutionContextの定義はこんな感じ。
@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem)
extends CustomExecutionContext(system, "database.dispatcher")
implicitパラメータで渡す。
class DatabaseService @Inject()(implicit executionContext: DatabaseExecutionContext) {
...
}
WSClientの改善
PlayのWSClient
はスタンドアロンで利用可能なplay-wsのラッパーになった。また、play-wsで使用されているライブラリはシャードされるようになったのでNettyがSpark、Playなどが使用しているNettyとコンフリクトすることがなくなった。
また、キャッシュ実装が存在する場合はHTTP Cachingがサポートされるようになった。
詳細は以下を参照:
タプルは配列にシリアライズされる。たとえば("foo", 2, "bar")
は["foo", 2, "bar"]
にレンダリングされる。
Play JSON 2.6.0はScala.jsもサポートしている。
libraryDependencies += "com.typesafe.play" %%% "play-json" % version
テストの改善
play.api.test
パッケージにテストのためのいくつかのユーティリティクラスが追加された。DIされるコンポーネントを使用したファンクショナルテストを簡単に行うことができるようになった。
Injecting
今まではこんな感じで書いていたものが、
"test" in new WithApplication() {
val executionContext = app.injector.instanceOf[ExecutionContext]
...
}
Injecting
トレイトを使って以下のように記述できるようになった。
"test" in new WithApplication() with Injecting {
val executionContext = inject[ExecutionContext]
...
}
StubControllerComponents
StubControllerComponentsFactory
でControllerComponents
のスタブを生成することができ、コントローラーのユニットテストで以下のように使用できる。
val controller = new MyController(stubControllerComponents())
StubBodyParser
同様にStubBodyParserFactory
でBodyParser
のスタブを生成することができ、コントローラーのユニットテストで使用できる。
val stubParser = stubBodyParser(AnyContent("hello"))
ファイルアップロードの改善
ファイルアップロードはTemporaryFile
(テンポラリファイルシステムに保存される)を使用する。理想的な挙動は不要になったTemporaryFile
は可能な限り早く削除されることだが、Play 2.5ではファイナライザを使用してGC時に削除されていた。しかし特定の条件下ではGCはタイムリーに発生しないため、バックグラウンドのクリーンアップ処理はFinalizableReferenceQueue
とPhantomReferenceを使用するように変更されていた。
TemporaryFile
は再度修正され、TemporaryFile
の参照はTemporaryFileCreator
トレイトから来るようになり、必要に応じて実装を差し替えることができるようになった。また、StandardCopyOption.ATOMIC_MOVE
を使用するatomicMoveWithFallback
メソッドが追加された。
TemporaryFileReaper
Akkaスケジューラを使用して定期的にテンポラリファイルを削除できるplay.api.libs.Files.TemporaryFileReaper
が追加された。デフォルトでは無効だがapplication.conf
で以下のようにすることで有効にできる。
play.temporaryFile {
reaper {
enabled = true
initialDelay = "5 minutes"
interval = "30 seconds"
olderThan = "30 minutes"
}
}
この設定は30分以上前のファイルを削除する。reaperはアプリケーションが起動してから5分後に開始し、30秒ごとにファイルシステムをチェックする。きちんと設定しないと時間のかかっているアップロード中のファイルを削除してしまう可能性があるので注意すること。