§依存性注入
依存性注入は、コンポーネントの動作と依存関係の解決を分離するのに役立つ、広く使用されている設計パターンです。Playは、JSR 330に基づく実行時依存性注入(このページで説明)と、Scalaでのコンパイル時依存性注入の両方をサポートしています。
実行時依存性注入と呼ばれるのは、依存関係グラフが実行時に作成、接続、検証されるためです。特定のコンポーネントの依存関係が見つからない場合、アプリケーションを実行するまでエラーは発生しません。
PlayはGuiceを標準でサポートしていますが、他のJSR 330実装をプラグインすることもできます。Guice wikiは、Guiceの機能とDI設計パターン全般について学ぶための優れたリソースです。
Playのsbtプラグインは、デフォルトでは特定の依存性注入フレームワークを提供していません。PlayのGuiceモジュールを使用する場合は、ライブラリの依存関係に明示的に追加してください。方法は次のとおりです。
libraryDependencies += guice
注: GuiceはJavaライブラリであり、このドキュメントの例ではGuiceの組み込みJava APIを使用しています。Scala DSLをご希望の場合は、scala-guiceまたはsse-guiceライブラリを使用することをお勧めします。
§動機
依存性注入は、いくつかの目標を達成します。
1. 同じコンポーネントに対して異なる実装を簡単にバインドできます。これは、モックの依存関係を使用してコンポーネントを手動でインスタンス化したり、代替実装を注入したりできるテストに特に役立ちます。
2. グローバルな静的状態を回避できます。静的ファクトリは最初の目標を達成できますが、状態が正しく設定されていることを確認する必要があります。特に、Playの(現在は非推奨の)静的APIは実行中のアプリケーションを必要とするため、テストの柔軟性が低下します。また、一度に複数のインスタンスを使用できるようにすることで、テストを並列実行できます。
Guice wikiには、これをより詳細に説明する良い例がいくつかあります。
§仕組み
Playは、いくつかの組み込みコンポーネントを提供し、BuiltinModuleなどのモジュールでそれらを宣言します。これらのバインディングは、デフォルトでは、コントローラーがコンストラクターに注入された、ルートコンパイラーによって生成されたルーターを含む、`Application`のインスタンスを作成するために必要なすべてを記述します。これらのバインディングは、Guiceやその他の実行時DIフレームワークで動作するように変換できます。
Playチームは、GuiceApplicationLoaderを提供するGuiceモジュールをメンテナンスしています。これは、Guiceのバインディング変換を行い、それらのバインディングを使用してGuiceインジェクターを作成し、インジェクターから`Application`インスタンスをリクエストします。
ScaldiやSpringなど、他のフレームワーク向けにこれを行うサードパーティのローダーもあります。
あるいは、Playは、コンパイル時にアプリを接続する純粋なScala実装を作成できるBuiltInComponentsトレイトを提供します。
デフォルトのバインディングとアプリケーションローダーのカスタマイズ方法については、以下で詳しく説明します。
§実行時DI依存関係の宣言
コントローラーなどのコンポーネントがあり、他のコンポーネントを依存関係として必要とする場合、@Injectアノテーションを使用して宣言できます。`@Inject`アノテーションは、フィールドまたはコンストラクターで使用できます。コンストラクターで使用することをお勧めします。例:
import javax.inject._
import play.api.libs.ws._
class MyComponent @Inject() (ws: WSClient) {
// ...
}
`@Inject`アノテーションは、クラス名の後、コンストラクターパラメーターの前に記述する必要があり、括弧が必要です。
また、Guiceには他にもいくつかの種類のインジェクションが付属していますが、コンストラクターインジェクションは、一般的にScalaで最も明確、簡潔、かつテストしやすいので、使用することをお勧めします。
Guiceは、コンストラクターに`@Inject`を持つクラスを明示的にバインドすることなく、自動的にインスタンス化できます。この機能はJust In Timeバインディングと呼ばれ、Guiceのドキュメントで詳しく説明されています。より高度な処理が必要な場合は、以下で説明するようにカスタムバインディングを宣言できます。
§コントローラーの依存性注入
Playのルートコンパイラーは、コントローラーをコンストラクターの依存関係として宣言するルータークラスを生成します。これにより、コントローラーをルーターに注入できます。
コントローラー名の前に`@`記号を付けると、特別な意味を持ちます。コントローラーが直接注入される代わりに、コントローラーの`Provider`が注入されます。これにより、たとえば、プロトタイプコントローラーや、循環依存関係を解消するためのオプションが可能になります。
§コンポーネントのライフサイクル
依存性注入システムは、注入されたコンポーネントのライフサイクルを管理し、必要に応じてそれらを作成し、他のコンポーネントに注入します。コンポーネントのライフサイクルは次のように動作します。
- **コンポーネントが必要になるたびに新しいインスタンスが作成されます。** コンポーネントが複数回使用される場合、デフォルトでは、コンポーネントの複数のインスタンスが作成されます。コンポーネントのインスタンスを1つだけにする場合は、シングルトンとしてマークする必要があります。
- **インスタンスは、必要になったときに遅延作成されます。** コンポーネントが他のコンポーネントによって使用されない場合、まったく作成されません。これは通常、望ましい動作です。ほとんどのコンポーネントは、必要になるまで作成する意味がありません。ただし、アプリケーションの起動時にすぐに、または他のコンポーネントで使用されていない場合でも、コンポーネントを起動したい場合があります。たとえば、アプリケーションの起動時にリモートシステムにメッセージを送信したり、キャッシュをウォームアップしたりする場合などです。eagerバインディングを使用すると、コンポーネントを強制的に作成できます。
- **インスタンスは自動的にはクリーンアップされません**。通常のガベージコレクション以外では。コンポーネントは、参照されなくなるとガベージコレクションされますが、フレームワークは、`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は、Playのシャットダウン時にコンポーネントを停止するためのフックを登録するために使用できるApplicationLifecycleコンポーネントを提供します。
import javax.inject._
import scala.concurrent.Future
import play.api.inject.ApplicationLifecycle
@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
val connection = connectToMessageQueue()
lifecycle.addStopHook { () => Future.successful(connection.stop()) }
// ...
}
`ApplicationLifecycle`は、作成時とは逆の順序ですべてのコンポーネントを停止します。これは、依存しているコンポーネントは、コンポーネントの停止フックで引き続き安全に使用できることを意味します。依存しているため、それらのコンポーネントは、コンポーネントが作成される前に作成されている必要があり、したがって、コンポーネントが停止されるまで停止されません。
**注:** 停止フックを登録するすべてのコンポーネントがシングルトンであることを確認することが非常に重要です。停止フックを登録するシングルトンではないコンポーネントは、コンポーネントが作成されるたびに新しい停止フックが登録されるため、メモリリークの原因となる可能性があります。
クリーンアップロジックは、調整されたシャットダウンを使用して実装することもできます。Play は内部で Pekko の調整されたシャットダウンを使用していますが、ユーザーランドコードでも使用できます。 ApplicationLifecycle#stop
は、調整されたシャットダウンタスクとして実装されています。主な違いは、ApplicationLifecycle#stop
はすべての停止フックを予測可能な順序で順番に実行するのに対し、調整されたシャットダウンは同じフェーズ内のすべてのタスクを並行して実行するため、高速になる可能性がありますが、予測不可能です。
§カスタムバインディングの提供
コンポーネントのトレイトを定義し、他のクラスをコンポーネントの実装ではなく、そのトレイトに依存させることは、良い習慣とされています。そうすることで、異なる実装を注入できます。たとえば、アプリケーションをテストするときにモック実装を注入できます。
この場合、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.name.Names
import com.google.inject.AbstractModule
class Module extends AbstractModule {
override 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 バインディングを設定するときに、Play の Configuration
を読み取ったり、ClassLoader
を使用したりすることが必要な場合があります。これらのオブジェクトにアクセスするには、モジュールのコンストラクターに追加します。
以下の例では、各言語の Hello
バインディングは設定ファイルから読み取られます。これにより、application.conf
ファイルに新しい設定を追加することで、新しい Hello
バインディングを追加できます。
import com.google.inject.name.Names
import com.google.inject.AbstractModule
import play.api.Configuration
import play.api.Environment
class Module(environment: Environment, configuration: Configuration) extends AbstractModule {
override 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.name.Names
import com.google.inject.AbstractModule
// A Module is needed to register bindings
class Module extends AbstractModule {
override def configure() = {
// Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello])
.asEagerSingleton()
bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello])
.asEagerSingleton()
}
}
先行シングルトンは、アプリケーションの起動時にサービスを起動するために使用できます。多くの場合、シャットダウンフックと組み合わせて使用され、アプリケーションの停止時にサービスがリソースをクリーンアップできるようにします。
import javax.inject._
import scala.concurrent.Future
import play.api.inject.ApplicationLifecycle
// This creates an `ApplicationStart` object once at start-up and registers hook for shut-down.
@Singleton
class ApplicationStart @Inject() (lifecycle: ApplicationLifecycle) {
// Shut-down hook
lifecycle.addStopHook { () => Future.successful(()) }
// ...
}
import com.google.inject.AbstractModule
class StartModule extends AbstractModule {
override def configure() = {
bind(classOf[ApplicationStart]).asEagerSingleton()
}
}
§Play ライブラリ
Play 用のライブラリを実装している場合は、アプリケーションで使用されている DI フレームワークに関係なく、ライブラリが標準で動作するように、DI フレームワークに依存しないようにするのが一般的です。このため、Play は DI フレームワークに依存しない方法でバインディングを提供するための軽量バインディング API を提供しています。
バインディングを提供するには、モジュールを実装して、提供するバインディングのシーケンスを返します。 Module
トレイトは、バインディングを構築するための DSL も提供します。
import play.api.inject._
import play.api.Configuration
import play.api.Environment
class HelloModule extends Module {
def bindings(environment: Environment, configuration: Configuration): Seq[play.api.inject.Binding[_]] = 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
メソッドは、Play のEnvironment
とConfiguration
を受け取ります。バインディングを動的に設定する場合は、これらにアクセスできます。- モジュールバインディングは、先行バインディングをサポートしています。先行バインディングを宣言するには、
Binding
の最後に.eagerly
を追加します。
フレームワーク間の互換性を最大限に高めるために、以下の点に注意してください。
- すべての DI フレームワークが Just-In-Time バインディングをサポートしているわけではありません。ライブラリが提供するすべてのコンポーネントが明示的にバインドされていることを確認してください。
- バインディングキーはシンプルにしてください。実行時の 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
は Bar
に依存し、Bar
は Baz
に依存し、Baz
は Foo
に依存します。そのため、これらのクラスのいずれもインスタンス化できません。この問題は、Provider
を使用することで回避できます。
import javax.inject.Inject
import javax.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
クラスを拡張できます。
オーバーライドできるメソッドはいくつかありますが、通常は builder
メソッドをオーバーライドします。このメソッドは、ApplicationLoader.Context
を読み取り、GuiceApplicationBuilder
を作成します。以下に、builder
の標準実装を示します。これは、好きなように変更できます。 GuiceApplicationBuilder
の使用方法については、Guice を使用したテストに関するセクションを参照してください。
import play.api.inject._
import play.api.inject.guice._
import play.api.ApplicationLoader
import play.api.Configuration
class CustomApplicationLoader extends GuiceApplicationLoader() {
override def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
val extra = Configuration("a" -> 1)
initialBuilder
.in(context.environment)
.loadConfig(context.initialConfiguration.withFallback(extra))
.overrides(overrides(context): _*)
}
}
ApplicationLoader
をオーバーライドする場合は、Play に指示する必要があります。次の設定を application.conf
に追加します。
play.application.loader = "modules.CustomApplicationLoader"
依存性注入に Guice を使用する必要はありません。 ApplicationLoader
をオーバーライドすることで、アプリケーションの初期化方法を制御できます。詳細については、次のセクションを参照してください。
§サブクラスに触れずにクラスに依存関係を追加する
多くのサブクラスを持つ可能性のあるベースクラスに新しい依存関係を追加したい場合があります。
それぞれに依存関係を直接提供することを避けるために、注入可能なフィールドとして追加できます。
このアプローチはクラスのテスト容易性を低下させる可能性があるため、注意して使用してください。
import com.google.inject.ImplementedBy
import com.google.inject.Inject
import com.google.inject.Singleton
import play.api.mvc._
@ImplementedBy(classOf[LiveCounter])
trait Counter {
def inc(label: String): Unit
}
object NoopCounter extends Counter {
override def inc(label: String): Unit = ()
}
@Singleton
class LiveCounter extends Counter {
override def inc(label: String): Unit = println(s"inc $label")
}
class BaseController extends ControllerHelpers {
// LiveCounter will be injected
@Inject
@volatile protected var counter: Counter = NoopCounter
def someBaseAction(source: String): Result = {
counter.inc(source)
Ok(source)
}
}
@Singleton
class SubclassController @Inject() (action: DefaultActionBuilder) extends BaseController {
def index = action {
someBaseAction("index")
}
}
このドキュメントに誤りを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを送信してください。質問やアドバイスがあれば、コミュニティフォーラムにアクセスして、コミュニティとの会話を始めてください。