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が案内されています。

- [Runtime Dependency Injection](https://www.playframework.com/documentation/ja/2.4.x/ScalaDependencyInjection)（実行時の動的なDI）
- [Compile Time Dependency Injection](https://www.playframework.com/documentation/ja/2.4.x/ScalaCompileTimeDependencyInjection)（コンパイル時の静的な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](https://github.com/flyway/flyway-play) と [scalikejdbc-play-support](https://github.com/scalikejdbc/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](https://github.com/adamw/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](https://github.com/adamw/macwire) 便利だよね！


## まとめ
- application.conf に `play.modules.enabled` を記述してモジュールが読み込まれるのは標準の `ApplicationLoader` を使っている場合のみ。
- コンパイル時DIを使う場合は自作の `ApplicationLoader` の中に自力でなんとかする必要がある。
- [macwire](https://github.com/adamw/macwire) 便利（大事なことなので）。