Alternative Play Framework on Tomcat としての Ktor 3 + Twirl 2024年末
要約
- ktor3 + Scala Templates のシンプル構成はとりあえず動作した(すげぇ)
- ただし、Twirl を先行ビルドする関係でビルドを2回にわける必要がある。
- Twirl の HTML コードを複雑に作り込んだケースで正しく動くかは未調査。
- ktor が war 化に対応しているので Tomcat 上で動作させることも可能
- トレードオフとしてビルド時間がそこそこかかる
- (そりゃ Kotlin も Scala もビルドしてるからね)
GitHub のサンプルプロジェクトはこちら
背景
Scala Templates をご存知だろうか。
正式名称を「Twirl」というライブラリ名で、Play Framework に標準搭載されている HTML テンプレートエンジンである。
JVM 系の数あるテンプレートエンジンと比較して使い勝手やパフォーマンスが良く、これを求めて Play Framework を採用する時代もあったそうな。(10 年前くらいの話らしい)
こんなに良いエンジンだと言われているのに、Play Framework 以外で採用されるケースをみたことがない。
みたことがない以前に、「令和の時代に Play Framework 使うやついるの?!」って声だけが強く聞こえてきそう。
で、「Play 以外で Twirl って動作するのかしら?」という疑問が湧いてきたので、年末の宿題がてら、いい感じのパターンがないか試してみることにした。(Ktor が好きなので、とりあえず Ktor 上で動かせたりしないかなと。)
Tomcat と War コンテナ
さて、話は変わって、現行の Play Framework では War コンテナとしてビルドすることが一切できない。3rd の War ビルドプラグインも Play Framework V2.4 を最後に開発が止まっている。(10 年も前の話...)
「最新の Play が War 化できないから、未だに Migration できないんですけど...」って恨み節が聞こえてくる。
試しに GitHub から War ビルドプラグインを Fork してアップデートしようとしたが、V2.4 時代の古い Play が抱える Stream ライブラリが、Akka Stream への置き換えられた事情を反映する必要があり、かつ Scala の開発にある程度精通していないとならず、Scala わからない私では手も足も出なかった。
もうちょっとシンプルな実装の読み替えで済むと思っていたが、思ったより修羅の道だった。
一方、今回使う予定の Ktor は、最新の V3 でも War ビルドに対応しているため、これに相乗りできたら最高かもと考えている。
環境
プロジェクト
ミニマムということで次の2つのみを考慮する。
- Ktor 3 (現行最新)
- Twirl
マシン
最終形の動作検証は次の環境でテストした
- Windows 11 + Java 11
- Ubuntu 24.10 + Java 17
一応、Java 11 or later が入っていれば、OS 問わずビルドと実行ができるはず。
手順
Ktor の準備
ベースフレームワークは Ktor なので、 Ktor Project Generator で作成して、そこに手を加えていく。
実行設定は Tomcat 用設定、Gradle-Kotlin(build.gradle.kts)で Generate する。(好みなので、読み替えができればなんでもいい)
また、War ビルドを有効にするドキュメントを参考にして、有効にしておく。
一応、Tomcat10 をターゲットにするつもりでおく。
もちろんこの時点で War 化ビルドは動く(当たり前だけど)
./gradlew war
./gradlew appRunWar
ちなみに、web.xml はこの位置に配置する。
- src
- main
- webapp
- WEB-INF
- web.xml
Scala プラグインの有効化
一般的な Gradle の Scala プラグインの有効方法を見て、そのとおりにセットアップする。
ビルドが通ったり実行できたりすることを確認するために、次の位置になんらかのコードを入れておくと良い。
- src
- main
- kotlin
- resources
- scala
- saurus
- plesio
- Dummy.scala
- webapp
Twirl の設定
Twirl はビルドを経て Scala コードにトランスパイルされる。
Play Framework 上でなら同じ Scala のビルドに乗るため特に考慮が要らないが、今回は Kotlin 領域から Twirl の Scala コードを参照できるような設定にする必要がある。
[versions]
twirl-version = "2.0.7"
scala3-version = "3.6.2"
...
[libraries]
twirl-api = { module = "org.playframework.twirl:twirl-api_3", version.ref = "twirl-version" }
scala3-library = { module = "org.scala-lang:scala3-library_3", version.ref = "scala3-version" }
commons-collections = { module = "commons-collections:commons-collections", version = "3.2.2" }
...
[plugins]
twirl = { id = "org.playframework.twirl", version.ref = "twirl-version" }
plugins {
alias(libs.plugins.twirl)
}
dependencies {
implementation(libs.twirl.api)
implementation(libs.scala3.library)
implementation(libs.commons.collections)
}
sourceSets {
main {
twirl {
srcDir("src/main/twirl")
sourceEncoding.set("UTF-8")
}
scala {
setSrcDirs(listOf("src/main/scala", "build/generated/sources/twirl/main"))
}
kotlin {
compileClasspath += files(tasks.compileScala.get().destinationDir)
}
}
}
twirl {
scalaVersion.set("3")
}
tasks {
compileScala {
dependsOn("compileTwirl")
options.encoding = "UTF-8"
scalaCompileOptions.isForce = true
}
}
最後に、なんらかシンプルなファイルをセットする。
- src
- main
- kotlin
- resources
- scala
- twirl
- saurus
- plesio
- hello.scala.html
- webapp
@(message: String = "hello")
<section id="top">
<div class="wrapper">
<h1>@message</h1>
<h2>Hello, World</h2>
</div>
</section>
Ktor から Twirl を呼び出す
Kotlin が Scala コードを認識できるようにするため、Scala, Twirl は Kotlin よりも先にビルドしないといけない。
――が、なぜか compileScala の実行依存に compileKotlin が存在している。(リファレンス読むと、compileJavaに依存しているって記述があるのだが...正直どうなっているかまでは本筋じゃないので調べてない。)
とにかく、Scala領域 だけを先行でビルドする必要がある。これを守らないと後述のimportが永遠にエラー扱いとなりビルドが通らなくなる。
ということで、次のコマンドで無理やりScalaだけビルドを通過させる。
./gradlew compileScala -x compileKotlin
(ちなみに Gradle 設定上での setDependsOn() で compileScala から compileKotlin を排除して実行すると、ビルドが終わらなくなる。さらに言えば、mustRunAfterで順序を無理矢理入れ替えようとすると、実行時に依存ループだと怒られる。)
ビルドが通ったなら、IDE などで次のコードを実装してもエラーにならなくなる。
(Ktor を使うなら、普通は IDEA を使うと思うので IDEA が正しく反応してくれるところまで確認した。)
...
import saurus.plesio.html.hello
...
get("/twirl"){
val res = hello.render("aaaa")
call.respondText(res.toString(), ContentType.Text.Html, HttpStatusCode.OK)
}
本来、Ktor でテンプレートエンジンを使う場合は、該当するエンジンに対応したプラグインを依存関係に入れてそれを呼び出すような実装になるが、今回はそれができないため、respondText の扱いにしつつ、ContentType や中身の状態を調整することで擬似的に再現する。
いざ実行
あとは、War ビルドして、実行して localhost:8080/twirl にアクセスして思った結果になるか確認する。
./gradlew compileScala -x compileKotlin # Twirl側の実装に変更があれば
./gradlew appRunWar
ちゃんと「aaaa」が変数として利用されている。ちゃんと動いて嬉しい。
残件
- 複雑な Twirl の HTML コードでも正しくレンダーできるか
- 特に複数ファイルにまたがる構造になっているとき
- [1/15 追記] 複数のScala TemplatesファイルをまたいだHTML構造になっていても、Twirlをビルドする際に、解決してclassファイルになってくれるので、エントリポイントのメソッドが複数になることはなさそう。
- リソースファイルとの連携、つまり Ktor 側の別処理と Twirl のレンダーが正しく連携されるか。
- [1/15 追記] スタンドアロンの場合は、相対パスなり絶対パスなりを組み立ててた先にStatic Resource設定を仕掛けておけばそれを勝手に拾ってくれる。(そもそもHTML部から呼び出す部分なのでTwirlあんま関係ない)
- [1/15 - ] ただし、warデプロイになると、serviceXXX.warだったらURLパス上に「http://localhost:8080/serviceXXX/test」といったパスが増えることになるため、リソース取得のためにうまく工夫をしないといけない。
- [1/15 追記] スタンドアロンの場合は、相対パスなり絶対パスなりを組み立ててた先にStatic Resource設定を仕掛けておけばそれを勝手に拾ってくれる。(そもそもHTML部から呼び出す部分なのでTwirlあんま関係ない)
- ktor3 自体が ktor2 でできていた構成と同じようにセッティングしてちゃんと動くか調べてない
- exposed とか Ktorの各種プラグインとかそういうのを盛った時もTwirlが正しく使えるかは要調査。(なんらかの組み合わせでBlank Pageが出るバグがあるような気がしている)
- [1/15 追記]ktor以外でもいける?
- もっと突き詰めたら、twirlだけ独立したプロジェクトに切り出して、FatJarとしてビルドしてしまえばktorに限らずどんな JVM WebFrameworks 上でも理論上は動作させることができる。
- FatJarでビルドしたTwirl(と自作のHTMLコード)を Javalin や Micronaut とかに投げ込んで呼び出したら普通にいけた。
所感
とりあえず、WAR コンテナとして Twirl を実行できるところまで確認できた。
できれば、もうちょっと調べてワンライナーでwarビルドにこぎつけたいところだが、実行順序の依存関係周りがだいぶアンタッチャブル感あるので、2回に分けて実行する今のやり方が最適解かもしれない。
この記事が、世の中の「Play Framework + War で飯食ってたのに、仕様変更によって地獄を見ているうえ、Java 8 の EOLが迫っていてめっちゃ困り果てている」人への一助になったりしたら幸いである。
ちなみに、この記事は
でインストールした Ubuntu 24.10 on MBP 2014 で執筆した。
Discussion