Play Framework 2.4 がリリースされたのは2015年5月なのですごく今更感はあるのですが結構ハマった&わかりやすい記事が見当たらなかったのでまとめておくことにします。
忙しい人は最後のまとめだけ読めばOKかもしれないです。
Play Framework 2.4 の Dependency Injection
Play Framework 2.4 では Dependency Injection (以下DI)がフレームワークでサポートされました。
……というよりも、フレームワークの標準機能でガッツリ利用されるようになりました。
マニュアルを読むと、2種類のDIが案内されています。
- Runtime Dependency Injection(実行時の動的なDI)
- Compile Time Dependency Injection(コンパイル時の静的なDI)
前者は Google Guice を利用した実装で、@Singleton
やら @Inject
やらのアノテーションがたくさん出てきてすごく Java っぽいです(Guice が Java 向けなので当たり前ですが)。
DIの方法は所謂「コンストラクタDI」で、依存性の注入先となるクラスのコンストラクタのパラメータとして依存するオブジェクトを受け取ります。
誰かもどこかで言っていましたが言語レベルでシングルトンオブジェクトが定義できる Scala で @Singleton
というアノテーションには少し違和感があります。
後者は特にこれと言った機能やライブラリは標準では提供されておらず、「コンパイル時に好きなようにDIすればいいじゃない」というスタイルです。
play.api.ApplicationLoader
を継承した独自クラスを作成し、標準のものと差し替えることで実現します。
詳しい方法などはマニュアルを参照してください。
型安全教徒としてはもちろん後者を選択したのですが、それによって苦労したというお話。
Play Module について
Play Framework 2.4 ではDIの他にも大きな変更がいくつかあり、そのうちのひとつが Plugin に代わって登場した Module です。
Plugin は play.api.Plugin
を継承し play.plugins に記述することで利用していましたが Module は play.api.inject.Module
を継承し application.conf に次のような記述をすることで利用することになりました。
play.modules.enabled += "my.module.MyModule"
今回利用しようとした Module は flyway-play と scalikejdbc-play-support の2つですが、いずれも README にて上記のような設定をすることで使える……と案内されていました。
……が、それはあくまで Guice による実行時DIを利用している場合のみのお話だった、というわけです。
Play Module がどのように読み込まれるか
さて、というわけで各モジュールを libraryDependencies
に追加して play.modules.enabled += ...
を記述しても一向にモジュールが動作している様子がないことがわかりました。
最初は原因もわからず右往左往していたのですが、どうにもならないので Playframework のコードを調べてみました。
まずは application.conf の play.modules.enabled
がどこで読み込まれているかです。
play.modules.enabled
で検索してみると Modules
オブジェクトの locate
メソッドで設定が読まれているようです。
def locate(environment: Environment, configuration: Configuration): Seq[Any] = {
val includes = configuration.getStringSeq("play.modules.enabled").getOrElse(Seq.empty)
val excludes = configuration.getStringSeq("play.modules.disabled").getOrElse(Seq.empty)
...
次に Modules.locate
がどこで呼び出されているかですが、これは GuiceableModule
オブジェクトの loadModules
メソッドです。
def loadModules(environment: Environment, configuration: Configuration): Seq[GuiceableModule] = {
Modules.locate(environment, configuration) map guiceable
}
オブジェクト名に Guice と出てきて嫌な予感が……と思ったのも束の間、さらに辿ると GuiceApplicationBuilder
そして GuiceApplicationLoader
に辿り着きました。
つまりコンパイル時DIを使って独自の ApplicationLoader
を定義、利用している場合はいくら application.conf に記述しても Module が読み込まれることはないのです。
コンパイル時DI + Play Module を利用する方法
いよいよ本題です。
しばらく悩んだのですが、flyway-play と scalikejdbc-play-support の各モジュールのコードを読んでいるといずれも PlayModule
というクラスから PlayInitializer
という初期化クラスを呼び出している、という構造になっています。
いずれの PlayModule
も PlayInitializer
のためのエントリポイント、というカンジかつ PlayInitializer
のコンストラクタさえ呼び出してしまえば Module の機能が利用できそうです。
というわけで次のようになります。
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext
class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) {
...
new scalikejdbc.PlayInitializer(applicationLifecycle, configuration)
new org.flywaydb.play.PlayInitializer(configuration, environment, webCommands)
}
上記はコンパイル時DIを手動でゴリゴリ頑張っている人向けです。macwire を使っているなら次のようにすればOKです。
import com.softwaremill.macwire._
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext
class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) {
...
wire[scalikejdbc.PlayInitializer]
wire[org.flywaydb.play.PlayInitializer]
}
macwire 便利だよね!
まとめ
- application.conf に
play.modules.enabled
を記述してモジュールが読み込まれるのは標準のApplicationLoader
を使っている場合のみ。 - コンパイル時DIを使う場合は自作の
ApplicationLoader
の中に自力でなんとかする必要がある。 - macwire 便利(大事なことなので)。