https://playframework.com/documentation/2.6.x/ScalaDependencyInjection を google 翻訳した
依存性注入
依存関係注入は、依存関係解決からコンポーネントの動作を分離するのに役立つ広く使用されている設計パターンです。 Playでは、 JSR 330 (このページで説明)に基づく実行時依存性注入と、Scalaでの コンパイル時依存性注入 の両方をサポートしています。
ランタイム依存性注入は、依存関係グラフは実行時に作成され、配線され、検証されるためそう呼ばれます。特定のコンポーネントに対して依存関係が見つからない場合は、アプリケーションを実行するまでエラーは発生しません。
Playでは Guice をすぐにサポートしていますが、他のJSR 330の実装をプラグインすることもできます。 Guice wiki は、GuiceとDIの設計パターンの一般的な機能を詳細に学習するための素晴らしいリソースです。
動機
依存性注入はいくつかの目標を達成します。
- 同じコンポーネントの異なる実装を簡単にバインドすることができます。これは、特に模擬依存関係を使用してコンポーネントをインスタンス化したり、代替実装をインジェクトすることができるテストに便利です。
- グローバルな静的状態を避けることができます。静的な工場では最初の目標を達成することができますが、状態が適切に設定されていることを確認するように注意する必要があります。特にPlayの(現在は廃止予定の)静的APIには実行中のアプリケーションが必要であり、テストの柔軟性が低下します。一度に複数のインスタンスを使用できるようにすると、テストを並行して実行することができます。
Guice wiki には、これをより詳細に説明する良い例がいくつかあります。
使い方
Playはいくつかのビルトインコンポーネントを提供し、 BuiltinModule などのモジュールで宣言します。これらのバインディングは、 Application
のインスタンスを作成するために必要なすべてのものを記述します。デフォルトでは、コンストラクタにコントローラを注入したルートコンパイラによって生成されたルータが含まれます。これらのバインディングは、Guiceやその他の実行時DIフレームワークで動作するように変換できます。
PlayチームはGuiceモジュールを管理し、 GuiceApplicationLoader を提供します。これはGuiceのバインディング変換を行い、Guiceインジェクタをそれらのバインディングで作成し、インジェクタから Application
インスタンスを要求します。
Scaldi や Spring を含む他のフレームワークでこれを行うサードパーティのローダーもあります。
代わりに、Playは BuiltInComponents トレイトを提供します。これにより、 コンパイル時 にアプリケーションを結ぶ純粋なScala実装を作成することができます。
デフォルトのバインディングとアプリケーションローダーをカスタマイズする方法を以下でより詳しく説明します。
ランタイムDI依存関係の宣言
コントローラーなどのコンポーネントがあり、依存関係として他のコンポーネントが必要な場合は、これを @Inject アノテーションを使用して宣言できます。 @Inject
アノテーションは、フィールドまたはコンストラクタで使用できます。コンストラクタで使用することをお勧めします。たとえば、次のようにします。
import javax.inject._
import play.api.libs.ws._
class MyComponent @Inject() (ws: WSClient) {
// ...
}
@Inject
アノテーションはクラス名の後でコンストラクタパラメータの前に来なければならないことに注意してください。
また、Guiceにはいくつかの種類の注入方法 がありますが、コンストラクタ注入は一般的にScalaで最も明確で簡潔でテスト可能ですので、使用することをお勧めします。
Guiceは、明示的にバインドせずに、コンストラクタ上の @Inject
を持つクラスを自動的にインスタンス化することができます。この機能は ジャストインタイムバインディング と呼ばれ、Guiceのドキュメントで詳しく説明されています。より洗練された何かをする必要がある場合は、以下で説明するカスタムバインディングを宣言することができます。
依存性注入コントローラ
Playに依存性注入コントローラを使用させるには2通りの方法があります。
注入経路ジェネレータ
デフォルトでは(2.5.0以降)、Playはルータが依存関係としてルーティングするすべてのコントローラを宣言し、コントローラに依存性を注入することができるルータを生成します。
注入されたルートジェネレータを特に有効にするには、 build.sbt
のビルド設定に次の行を追加します。
routesGenerator := InjectedRoutesGenerator
注入されたルートジェネレータを使用する場合、アクションの先頭に @
記号を付けるのは特別な意味を持ちます。コントローラが直接注入されるのではなく、コントローラのプロバイダが注入されます。これは、例えば、プロトタイプコントローラ、および循環依存性を破るためのオプションを可能にする。
静的経路ジェネレータ
すべてのアクションが静的メソッドであることを前提とした従来の(2.5.0より前の)スタティックルートジェネレータを使用するようにPlayを設定することができます。プロジェクトを構成するには、build.sbtに次の行を追加します。
routesGenerator := StaticRoutesGenerator
注入された経路ジェネレータを常に使用することをお勧めします。スタティックルートジェネレータは、既存のプロジェクトですべてのコントローラを一度に静的にする必要がないように、移行を支援するツールとして主に存在します。
スタティックルートジェネレーターを使用している場合は、アクションの先頭に@を付けて、アクションに注入されたコントローラーがあることを示すことができます。
GET /some/path @controllers.Application.index
コンポーネントのライフサイクル
依存性注入システムは、注入されたコンポーネントのライフサイクルを管理し、必要に応じてそれらを作成し、それらを他のコンポーネントに注入します。コンポーネントのライフサイクルの仕組みは次のとおりです。
- 新しいインスタンスは、コンポーネントが必要になるたびに作成されます。 コンポーネントを複数回使用すると、デフォルトでコンポーネントの複数のインスタンスが作成されます。コンポーネントのインスタンスが1つしか必要ない場合は、それをシングルトンとしてマークする必要があります。
- インスタンスは、必要なときに遅延して作成されます。 あるコンポーネントが別のコンポーネントによって使用されることがない場合、そのコンポーネントはまったく作成されません。これは通常あなたが望むものです。ほとんどのコンポーネントでは、必要になるまでポイントを作成しません。しかし、場合によっては、コンポーネントをすぐに起動させたい場合や、別のコンポーネントで使用していなくても起動したい場合があります。たとえば、リモートシステムにメッセージを送信したり、アプリケーションの起動時にキャッシュをウォームアップしたりすることができます。早期バインディングを使用すると、コンポーネントをすぐに作成することができます。
- インスタンスは、通常のガベージコレクションを超えて自動的にはクリーンアップされません。 コンポーネントは参照されなくなったときにガベージコレクトされますが、フレームワークはコンポーネントをシャットダウンするために何もしません。ただし、Playは、ApplicationLifecycleと呼ばれる特別なタイプのコンポーネントを提供しています.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には、Playがシャットダウンしたときにコンポーネントを停止するためにフックを登録するために使用できる ApplicationLifecycle コンポーネントが用意されています。
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
は、作成されたときと逆の順序ですべてのコンポーネントを停止します。つまり、依存しているコンポーネントは、コンポーネントのストップフックで安全に使用できます。コンポーネントに依存しているため、コンポーネントが作成される前に作成されている必要があります。したがって、コンポーネントが停止するまで停止されません。
注意: ストップフックを登録するすべてのコンポーネントがシングルトンであることを確認することは非常に重要です。ストップフックを登録する非シングルトンコンポーネントは、コンポーネントが作成されるたびに新しいストップフックが登録されるため、メモリリークの原因となる可能性があります。
カスタムバインディングの提供
コンポーネントの特性を定義し、そのコンポーネントの実装ではなく、その特性に依存する他のクラスを持つことをお勧めします。これを行うことで、アプリケーションのテスト時に模擬実装を挿入するなど、さまざまな実装を注入できます。
この場合、DIシステムは、どのインプリメンテーションをその特性に結び付けるべきかを知る必要があります。これを宣言することを推奨する方法は、PlayアプリケーションをPlayのエンドユーザーとして作成するか、他のPlayアプリケーションが使用するライブラリを作成するかによって異なります。
Play アプリケーション
Playアプリケーションは、アプリケーションが使用しているDIフレームワークで提供されているメカニズムを使用することをお勧めします。 PlayではバインディングAPIが提供されていますが、このAPIは多少制限されており、使用しているフレームワークの力をフルに活用することはできません。
PlayはGuiceをすぐにサポートするため、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
}
プログラム的なバインディング
より複雑な状況では、 @Named アノテーションで修飾された1つの特性の実装が複数ある場合など、より複雑なバインディングを提供することができます。 このような場合、カスタムGuice モジュール を実装することができます:
import com.google.inject.AbstractModule
import com.google.inject.name.Names
class Module extends AbstractModule {
def configure() = {
bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello])
bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello])
}
}
このモジュールを Module
と呼び、それをルートパッケージに入れると、自動的にPlayに登録されます。 別の名前を付けたり、別のパッケージに入れる場合は、完全修飾クラス名を application.conf
の play.modules.enabled
リストに追加して、Playに登録することができます。
play.modules.enabled += "modules.HelloModule"
また、無効なモジュールにモジュールを追加して、ルートパッケージ内のModuleというモジュールの自動登録を無効にすることもできます。
play.modules.disabled += "Module"
設定可能なバインディング
Guiceバインディングを設定するときに、PlayConfiguration
を読み込んだり、 ClassLoader
を使用したりすることがあります。 これらのオブジェクトにアクセスするには、モジュールのコンストラクタにオブジェクトを追加します。
以下の例では、各言語の Hello
バインディングが設定ファイルから読み込まれます。 これにより、 application.conf
ファイルに新しい設定を追加することで、新しい Hello
バインディングを追加することができます。
import com.google.inject.AbstractModule
import com.google.inject.name.Names
import play.api.{ Configuration, Environment }
class Module(
environment: Environment,
configuration: Configuration) extends AbstractModule {
def configure() = {
// Expect configuration like:
// hello.en = "myapp.EnglishHello"
// hello.de = "myapp.GermanHello"
val helloConfiguration: Configuration =
configuration.getOptional[Configuration]("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.get[String](l)
val bindingClass: Class[_ <: Hello] =
environment.classLoader.loadClass(bindingClassName)
.asSubclass(classOf[Hello])
bind(classOf[Hello])
.annotatedWith(Names.named(l))
.to(bindingClass)
}
}
}
注意: ほとんどの場合、コンポーネントを作成するときに Configuration
にアクセスする必要がある場合、 Configuration
オブジェクトをコンポーネント自体またはコンポーネントの Provider
に挿入する必要があります。 次に、コンポーネントの作成時に Configuration
を読み取ることができます。 通常、コンポーネントのバインディングを作成するときに、 Configuration
を読み取る必要はありません。
早期バインディング
上記のコードでは、使用するたびに新しい EnglishHello
オブジェクトと GermanHello
オブジェクトが作成されます。 これらのオブジェクトを作成するのは高価なため、一度しか作成しない場合は、上記のように @Singleton
アノテーションを使用する必要があります。 それらを一度作成し、アプリケーションが起動したときにすぐに作成したい場合は、 Guiceの早期シングルトンバインディング を使用できます。
import com.google.inject.AbstractModule
import com.google.inject.name.Names
class Module 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シングルトンは、アプリケーションの起動時にサービスを起動するために使用できます。 アプリケーションが停止したときにサービスがリソースをクリーンアップできるように、シャットダウンフックと組み合わされることがよくあります。
Playライブラリ
Play用のライブラリを実装している場合は、DIフレームワークには無関係で、アプリケーションで使用されているDIフレームワークに関係なくライブラリをそのまま使用できます。 このため、Playでは、DIフレームワークに依存しない方法でバインディングを提供するための軽量バインディングAPIを提供しています。
バインディングを提供するには、提供するバインディングのシーケンスを返す Module を実装します。 Moduleのトレイトは、バインディングを構築するためのDSLも提供します。
import play.api.inject._
class HelloModule extends Module {
def bindings(environment: Environment,
configuration: Configuration) = Seq(
bind[Hello].qualifiedWith("en").to[EnglishHello],
bind[Hello].qualifiedWith("de").to[GermanHello]
)
}
このモジュールは、 reference.conf
の play.modules.enabled
リストに追加することで自動的にPlayに登録されます:
play.modules.enabled += "com.example.HelloModule"
- Module
bindings
メソッドは、PlayEnvironment
とConfiguration
を取ります。 バインディングを動的に構成する場合は、これらにアクセスできます。 - モジュールバインディングは、早期バインディングをサポートします。 早期バインディングを宣言するには、バインディングの最後に
.eagerly
を追加します。
クロスフレームワークの互換性を最大限に高めるには、次の点に注意してください。
- すべてのDIフレームワークがジャストインタイムバインディングをサポートしているわけではありません。 ライブラリが提供するすべてのコンポーネントが明示的にバインドされていることを確認してください。
- バインディングキーを簡単に保つようにしてください。異なるランタイムDIフレームワークでは、キーとは何か、それがどのようにユニークであるべきか、そうでないかについてはまったく異なる見解があります。
モジュールを除く
ロードしたくないモジュールがある場合は、application.confのplay.modules.disabledプロパティにそのモジュールを追加して除外できます。
play.modules.disabled += "play.api.db.evolutions.EvolutionsModule"
循環依存関係の管理
循環依存は、コンポーネントの1つが元のコンポーネントに依存する(直接的または間接的に)別のコンポーネントに依存する場合に発生します。例えば:
import javax.inject.Inject
class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Foo)
この場合、 Foo
は Foo
に依存する Baz
に依存する Bar
に依存します。したがって、これらのクラスのいずれかをインスタンス化することはできません。 Provider
を使用してこの問題を回避することができます:
import javax.inject.{ Inject, Provider }
class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Provider[Foo])
一般的に、循環依存関係は、コンポーネントをより原子的な方法で分割するか、依存するより具体的なコンポーネントを見つけることで解決できます。一般的な問題は Application
への依存です。あなたのコンポーネントが Application
に依存している場合、それはその仕事をするために完全なアプリケーションが必要であるということです。典型的にそうではありません。依存関係は、必要な特定の機能を持つより具体的なコンポーネント( Environment
など)に依存する必要があります。最後の手段として、 Provider[Application]
を注入することで問題を回避することができます。
上級者:GuiceApplicationLoaderの拡張
Playのランタイム依存性注入は、 GuiceApplicationLoader クラスによってブートストラップされます。このクラスはすべてのモジュールをロードし、モジュールをGuiceにフィードし、Guiceを使用してアプリケーションを作成します。 Guiceがアプリケーションを初期化する方法を制御したい場合は、 GuiceApplicationLoader
クラスを拡張できます。
オーバーライドする方法はいくつかありますが、通常はビルダーメソッドをオーバーライドします。このメソッドは、 ApplicationLoader.Context を読み取り、 GuiceApplicationBuilder を作成します。以下に、あなたが好きな方法で変更することができる builder
の標準的な実装を見ることができます。 GuiceApplicationBuilder
の使い方については、 Guiceのテストに関するセクション を参照してください。
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"
あなたは依存性注入のためにGuiceを使用することに限定されません。 ApplicationLoader
をオーバーライドすることで、アプリケーションの初期化方法を制御できます。 次のセクション で詳しく説明します。