パンダのメモ帳

技術系のネタをゆるゆると

Play Framework 2.4 + コンパイル時DIで Play Module を利用する方法

Play Framework 2.4 がリリースされたのは2015年5月なのですごく今更感はあるのですが結構ハマった&わかりやすい記事が見当たらなかったのでまとめておくことにします。

忙しい人は最後のまとめだけ読めばOKかもしれないです。

Play Framework 2.4 の Dependency Injection

Play Framework 2.4 では Dependency Injection (以下DI)がフレームワークでサポートされました。 ……というよりも、フレームワークの標準機能でガッツリ利用されるようになりました。

マニュアルを読むと、2種類のDIが案内されています。

前者は Google Guice を利用した実装で、@Singleton やら @Inject やらのアノテーションがたくさん出てきてすごく Java っぽいです(GuiceJava 向けなので当たり前ですが)。 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-playscalikejdbc-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 という初期化クラスを呼び出している、という構造になっています。

いずれの PlayModulePlayInitializer のためのエントリポイント、というカンジかつ 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 便利(大事なことなので)。