Runtime Dependency Injection

元の文献: ScalaDependencyInjection

Dependency injectionはコンポーネントを切り分ける方法です。
そのためお互い直接は依存せず、互いに実装が注入されます。

従来の常識を覆し、PlayはJSR330に従ったruntime dependency injectionを提供します。
runtime dependency injectionはdependency graphが生成されることからwired and validatedとしてのランタイムと呼ばれます。
もし、特定のコンポーネントに依存関係が無ければ、アプリケーションはエラーにならないでしょう。

一方、Playはcompile time dependency injectionも提供しています。

・・・

Playで利用されるのはJSR330のデフォルト実装であるGuiceです。
ただ、他のJSR330も利用できます。

依存関係の制限

Controllerのようなコンポーネントがあり他のコンポーネントと依存しているとき、@injectアノテーションで宣言することができます。
@Injectアノテーションはフィールドやコンストラクタで使うことができますが、例のようにコンストラクタで使用する事を推奨します。

import javax.inject._
import play.api.libs.ws._

class MyComponent @Inject() (ws: WSClient) {
  // ...
}

@Injectアノテーションはクラス名の後、コンストラクタパラメータの前にカッコつきで宣言しなければいけません。

依存関係が注入されたコントローラ

Playで依存関係が注入されたコントローラを扱う方法は二通りあります。

Injected routes generator

デフォルトでPlayは全てのメソッドが静的メソッドであるという前提に静的なルータを生成します。
InjectedRoutesGeneratorを作成するには、…

全ての場合にInjectedRoutesGeneratorを使用することをお勧めします。

InjectedRoutesGeneratorを有効にするには、build.sbtに以下を追加する必要があります。

routesGenerator := InjectedRoutesGenerator

Injected actions

もし静的なルート生成を利用するなら、actionの前に@をつけることで対応できます。

GET        /some/path           @controllers.Application.index

コンポーネントのライフサイクル

DIシステムは注入されたコンポーネントのライフサイクルを管理します。
必要に応じてコンポーネントを生成したり、別のコンポーネントに注入したりします。
以下はコンポーネントがどのようなライフサイクルで動くかを示しています。

コンポーネントが必要になる度に新しいインスタンスが生成される

もしコンポーネントが2度以上使われる場合、デフォルトでは複数のインスタンスが生成されます。
インスタンスを1つに限定したければ、@Singleton宣言をしてください。

インスタンスは必要になったとき遅延生成される

もしコンポーネントが他のコンポーネントから利用されることがなければ、作成されることはありません。
大抵の場合は期待通りの挙動でしょう。
ほとんどのコンポーネントは必要とされるまで作成される理由もないはずです。
しかし、いくつかのケースでは他から利用されなくてもすぐに作成してほしいケースがあります。
例えば、…

Eager bindingsの仕組みを使えばそれを実現できます。

標準的なGC以上にインスタンスは自動的にクリーンアップされない

コンポーネントは他から参照されなくなればGCされます。
しかし、フレームワークはcloseメソッドのようなものを呼び出してコンポーネントをシャットダウンするため、特別そのようなことはしません。
ただ、PlayはApplicationLifecycleと呼ばれる特殊なタイプのコンポーネントを提供しており、アプリケーションを停止するとき登録されたコンポーネントをシャットダウンすることができます。

シングルトン

時々、状態を保持したいコンポーネントがあります。
キャッシュであったり、外部リソースコレクションであったり、生成するのが大変なコンポーネントであったり..
これらのケースではコンポーネントはただ1つであることが重要です。
@Singletonアノテーションを付けることでこれらを実現できます。

import javax.inject._

@Singleton
class CurrentSharePrice {
  @volatile private var price = 0

  def set(p: Int) = price = p
  def get = price
}

停止/クリーンアップ

Playをシャットダウンしたとき、スレッドプールのようにいくつかのコンポーネントはクリーンアップが必要です。
PlayはApplicationLifecycleコンポーネントを提供しており、Playがシャットダウンされたときに各コンポーネントを停止するフックを登録することができます。

import scala.concurrent.Future
import javax.inject._
import play.api.inject.ApplicationLifecycle

@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
  val connection = connectToMessageQueue()
  lifecycle.addStopHook { () =>
    Future.successful(connection.stop())
  }

  //...
}

ApplicationLifecycleは生成された時とは逆の順番で全てのコンポーネントを停止します。

停止フックを登録するコンポーネントが全てSingletonであることを確認することはとても大事です。
ミスがあるとメモリリークが発生します。
なぜなら、停止フックが登録される度にコンポーネントが作成されるからです。

カスタムバインディングの提供

コンポーネントの実装に依存するより、コンポーネントのトレイとに依存する方が良いでしょう。
そうすることによって、異なる実装を注入することが可能になります。
例えば、テストのときにMockを注入することができます 。

このようなケースで、DIシステムはトレイトにどの実装が注入されるべきかを知る必要があります。
あなたがエンドユーザとしてPlayアプリケーションを利用するのか、はたまたPlayアプリケーションで利用されるライブラリを作成するのかによってオススメの方法は変わります。

Playアプリケーションを利用する場合

Playは従来の常識を覆しGuiceをサポートしているので、下記例のようにバインディングできます。

アノテーションによるバインディング

最もシンプルな例はGuiceの@ImplementedByアノテーションを使うことです。

import com.google.inject.ImplementedBy

@ImplementedBy(classOf[EnglishHello])
trait Hello {
  def sayHello(name: String): String
}

class EnglishHello extends Hello {
  def sayHello(name: String) = "Hello " + name
}

プログラムによるバインディング

いくつかの複雑な状況ではもっと複雑なバインディングをしたいでしょう。
例えば、1つのトレイとが@namedアノテーションによって複数の実装を持ち得る場合です。
このケースでは、Guiceのカスタムモジュールを使用することで実現できます。

import com.google.inject.AbstractModule
import com.google.inject.name.Names

class HelloModule extends AbstractModule {
  def configure() = {

    bind(classOf[Hello])
      .annotatedWith(Names.named("en"))
      .to(classOf[EnglishHello])

    bind(classOf[Hello])
      .annotatedWith(Names.named("de"))
      .to(classOf[GermanHello])
  }
}

このモジュールはPlayに登録するためには、application.confに以下の設定が必要です。

play.modules.enabled += "modules.HelloModule"

構成によるバインディング

Guiceのバインディング設定をしていると、たまにPlayのConfigurationClassLoaderを読み込みたいときがあるかもしれません。
コンストラクタから上記を実現できます。

下記の例だと、それぞれの言語における「Hello」が設定ファイルから読み込まれています。
application.confに新しい設定を追加することで、新しい「Hello」を追加バインディングできます。

import com.google.inject.AbstractModule
import com.google.inject.name.Names
import play.api.{ Configuration, Environment }

class HelloModule(
  environment: Environment,
  configuration: Configuration) extends AbstractModule {
  def configure() = {
    // Expect configuration like:
    // hello.en = "myapp.EnglishHello"
    // hello.de = "myapp.GermanHello"
    val helloConfiguration: Configuration =
      configuration.getConfig("hello").getOrElse(Configuration.empty)
    val languages: Set[String] = helloConfiguration.subKeys
    // Iterate through all the languages and bind the
    // class associated with that language. Use Play's
    // ClassLoader to load the classes.
    for (l <- languages) {
      val bindingClassName: String = helloConfiguration.getString(l).get
      val bindingClass: Class[_ <: Hello] =
        environment.classLoader.loadClass(bindingClassName)
        .asSubclass(classOf[Hello])
      bind(classOf[Hello])
        .annotatedWith(Names.named(l))
        .to(bindingClass)
    }
  }
}

Eagerバインディング

上記コードでは、EnglishHelloGermanHelloは使われるたびに作成されます。
1度だけ作成すればいいなら、恐らく無駄なので@Singletonアノテーションを使うべきです。
もし、アプリケーションスタート時に1度作るならばGuice’s eager singleton bindingを使うことができます。

import com.google.inject.AbstractModule
import com.google.inject.name.Names

class HelloModule extends AbstractModule {
  def configure() = {

    bind(classOf[Hello])
      .annotatedWith(Names.named("en"))
      .to(classOf[EnglishHello]).asEagerSingleton

    bind(classOf[Hello])
      .annotatedWith(Names.named("de"))
      .to(classOf[GermanHello]).asEagerSingleton
  }
}

Eager singletonsはアプリケーションが開始してサービスが立ち上がるときに使用されます。
それらは度々シャットダウンフックと重なる為、サービスはアプリケーション停止時にリソースをクリーンアップします。

Playアプリケーション用ライブラリを作成する場合

…省略

【応用】GuiceApplicationLoader

Playのruntime dependency injectionGuiceApplicationLoaderによって動いています。
このクラスは全てのモジュールを読み込み、Guiceの中に共有し、それからアプリケーションを生成するためにGuiceを使用します。
どうやってGuiceがアプリケーションを初期化するかコントロールしたい場合、GuiceApplicationLoaderを継承することができます。

import play.api.ApplicationLoader
import play.api.Configuration
import play.api.inject._
import play.api.inject.guice._

class CustomApplicationLoader extends GuiceApplicationLoader() {
  override def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
    val extra = Configuration("a" -> 1)
    initialBuilder
      .in(context.environment)
      .loadConfig(extra ++ context.initialConfiguration)
      .overrides(overrides(context): _*)
  }
}

ApplicationLoaderをオーバーライドしたときはPlayに知らせてあげる必要があります。
application.confに以下の設定を追加して下さい。

play.application.loader = "modules.CustomApplicationLoader"

DIのためのGuiceの使い方は制限されていません。
ApplicationLoaderをオーバーライドすることによって、アプリケーションの初期化方法をコントロールすることができます。