§依存性注入
依存性注入は、コンポーネントの動作と依存関係の解決を分離するのに役立つ、広く利用されている設計パターンです。コンポーネントは、通常はコンストラクタパラメータとして依存関係を宣言し、依存性注入フレームワークはそれらのコンポーネントを結び付けるのに役立ちます。そのため、手動で行う必要はありません。
Playは、JSR 330に基づく依存性注入サポートを標準で提供しています。Playに付属するデフォルトのJSR 330実装はGuiceですが、他のJSR 330実装をプラグインすることもできます。Playが提供するGuiceモジュールを有効にするには、build.sbtのライブラリ依存関係に`guice`があることを確認してください。例:
libraryDependencies += guice
Guice wikiは、Guiceの機能とDI設計パターン全般について学ぶための優れたリソースです。
§動機
依存性注入は、いくつかの目標を達成します。
1. 同じコンポーネントに対して異なる実装を簡単にバインドできます。これは、特にテストに役立ちます。テストでは、モックの依存関係を使用してコンポーネントを手動でインスタンス化したり、代替実装を注入したりできます。
2. グローバルな静的状態を回避できます。静的ファクトリは最初の目標を達成できますが、状態が正しく設定されていることを確認する必要があります。特に、Playの(現在は非推奨の)静的APIには実行中のアプリケーションが必要です。そのため、テストの柔軟性が低下します。また、一度に複数のインスタンスを使用できるようにすることで、テストを並列実行できます。
Guice wikiには、これをより詳細に説明する良い例がいくつかあります。
§仕組み
Playは、いくつかの組み込みコンポーネントを提供し、BuiltinModuleなどのモジュールでそれらを宣言します。これらのバインディングは、`Application`のインスタンスを作成するために必要なすべてを記述します。デフォルトでは、ルートコンパイラによって生成されたルーターが含まれており、コントローラーがコンストラクターに注入されます。これらのバインディングは、Guiceや他のランタイムDIフレームワークで動作するように変換できます。
Playチームは、GuiceApplicationLoaderを提供するGuiceモジュールを保守しています。これは、Guiceのバインディング変換を行い、それらのバインディングを使用してGuiceインジェクターを作成し、インジェクターから`Application`インスタンスをリクエストします。
Springなど、他のフレームワーク向けにこれを行うサードパーティのローダーもあります。
デフォルトのバインディングとアプリケーションローダーのカスタマイズ方法については、以下で詳しく説明します。
§依存関係の宣言
コントローラーなどのコンポーネントがあり、他のコンポーネントを依存関係として必要とする場合、@Injectアノテーションを使用して宣言できます。`@Inject`アノテーションは、フィールドまたはコンストラクターで使用できます。たとえば、フィールドインジェクションを使用するには、次のようにします。
import javax.inject.*;
import play.libs.ws.*;
public class MyComponent {
@Inject WSClient ws;
// ...
}
これらは*インスタンス*フィールドであることに注意してください。静的フィールドを注入することは、カプセル化を損なうため、一般的には意味がありません。
コンストラクターインジェクションを使用するには、次のようにします。
import javax.inject.*;
import play.libs.ws.*;
public class MyComponent {
private final WSClient ws;
@Inject
public MyComponent(WSClient ws) {
this.ws = ws;
}
// ...
}
フィールドインジェクションの方が短くなりますが、アプリケーションではコンストラクターインジェクションを使用することをお勧めします。ユニットテストでは、クラスのインスタンスを作成するためにすべてのコンストラクタ引数を渡す必要があり、コンパイラはすべての依存関係が存在することを確認するため、最もテスト可能です。また、フィールドの「魔法の」設定が行われていないため、何が起こっているのかを理解しやすいです。DIフレームワークは、手動で記述できるのと同じコンストラクタ呼び出しを自動化しているだけです。
Guiceには、場合によっては役立つ他のいくつかの種類のインジェクションもあります。静的を使用するアプリケーションを移行する場合、静的インジェクションのサポートが役立つ場合があります。
Guiceは、明示的にバインドすることなく、コンストラクターに`@Inject`を持つクラスを自動的にインスタンス化できます。この機能はJust In Timeバインディングと呼ばれ、Guiceのドキュメントで詳しく説明されています。より高度な処理を行う必要がある場合は、以下で説明するようにカスタムバインディングを宣言できます。
§コントローラーの依存性注入
Playのルートコンパイラは、コントローラーをコンストラクターの依存関係として宣言するルータークラスを生成します。これにより、コントローラーをルーターに注入できます。
注入されたルートジェネレーターを具体的に有効にするには、`build.sbt`のビルド設定に以下を追加します。
routesGenerator := InjectedRoutesGenerator
コントローラー名の前に`@`記号を付けると、特別な意味を持ちます。コントローラーが直接注入される代わりに、コントローラーの`Provider`が注入されます。これにより、たとえば、プロトタイプコントローラーが可能になり、循環依存関係を解消するためのオプションが提供されます。
§コンポーネントのライフサイクル
依存性注入システムは、注入されたコンポーネントのライフサイクルを管理し、必要に応じてそれらを作成して他のコンポーネントに注入します。コンポーネントのライフサイクルは次のように機能します。
- **コンポーネントが必要になるたびに新しいインスタンスが作成されます**。コンポーネントが複数回使用される場合、デフォルトでは、コンポーネントの複数のインスタンスが作成されます。コンポーネントのインスタンスを1つだけにする場合は、シングルトンとしてマークする必要があります。
- **インスタンスは、必要なときに遅延作成されます**。コンポーネントが他のコンポーネントによって使用されない場合、それはまったく作成されません。これは通常、あなたが望むことです。ほとんどのコンポーネントでは、必要になるまで作成する意味がありません。ただし、アプリケーションの起動時にすぐに、または他のコンポーネントで使用されていない場合でも、コンポーネントを起動したい場合があります。たとえば、アプリケーションの起動時にリモートシステムにメッセージを送信したり、キャッシュをウォームアップしたりする場合があります。 eager bindingを使用することで、コンポーネントを強制的に作成できます。
- **インスタンスは通常のガベージコレクション以外では自動的にクリーンアップされ*ません*。コンポーネントは参照されなくなるとガベージコレクションされますが、フレームワークは`close`メソッドの呼び出しなど、コンポーネントをシャットダウンするための特別な処理は行いません。ただし、Playは`ApplicationLifecycle`と呼ばれる特別なタイプのコンポーネントを提供しています。これにより、アプリケーションの停止時にシャットダウンするコンポーネントを登録できます。
§シングルトン
キャッシュ、外部リソースへの接続など、状態を保持するコンポーネントがある場合、またはコンポーネントの作成にコストがかかる場合があります。このような場合、そのコンポーネントのインスタンスが1つだけであることが重要になる場合があります。これは、@Singletonアノテーションを使用して実現できます。
import javax.inject.*;
@Singleton
public class CurrentSharePrice {
private volatile int price;
public void set(int p) {
price = p;
}
public int get() {
return price;
}
}
§停止/クリーンアップ
Playのシャットダウン時に、例えばスレッドプールを停止するなど、一部のコンポーネントをクリーンアップする必要がある場合があります。Playは、Playのシャットダウン時にコンポーネントを停止するためのフックを登録するために使用できるApplicationLifecycleコンポーネントを提供します。
import java.util.concurrent.CompletableFuture;
import javax.inject.*;
import play.inject.ApplicationLifecycle;
@Singleton
public class MessageQueueConnection {
private final MessageQueue connection;
@Inject
public MessageQueueConnection(ApplicationLifecycle lifecycle) {
connection = MessageQueue.connect();
lifecycle.addStopHook(
() -> {
connection.stop();
return CompletableFuture.completedFuture(null);
});
}
// ...
}
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(EnglishHello.class)
public interface Hello {
String sayHello(String name);
}
public class EnglishHello implements Hello {
public String sayHello(String name) {
return "Hello " + name;
}
}
§プログラムによるバインディング
より複雑な状況では、@Namedアノテーションで修飾された1つのインターフェースの複数の実装がある場合など、より複雑なバインディングを提供したい場合があります。このような場合は、カスタムGuice Moduleを実装できます。
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
public class Module extends AbstractModule {
protected void configure() {
bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class);
bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class);
}
}
このモジュールをModule
と呼び、ルートパッケージに配置すると、Playに自動的に登録されます。または、別の名前を付けたり、別のパッケージに配置したりする場合は、application.conf
のplay.modules.enabled
リストに完全修飾クラス名を追加することで、Playに登録できます。
play.modules.enabled += "modules.HelloModule"
また、無効化されたモジュールにルートパッケージにあるModule
という名前のモジュールを追加することで、その自動登録を無効にすることもできます。
play.modules.disabled += "Module"
§設定可能なバインディング
Guiceバインディングを設定するときに、Config
を読み取ったり、ClassLoader
を使用したりする場合があります。これらのオブジェクトにアクセスするには、モジュールのコンストラクターに追加します。
以下の例では、各言語のHello
バインディングは設定ファイルから読み取られます。これにより、application.conf
ファイルに新しい設定を追加することで、新しいHello
バインディングを追加できます。
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import play.Environment;
public class Module extends AbstractModule {
private final Environment environment;
private final Config config;
public Module(Environment environment, Config config) {
this.environment = environment;
this.config = config;
}
protected void configure() {
// Expect configuration like:
// hello.en = "myapp.EnglishHello"
// hello.de = "myapp.GermanHello"
final Config helloConf = config.getConfig("hello");
// Iterate through all the languages and bind the
// class associated with that language. Use Play's
// ClassLoader to load the classes.
helloConf
.entrySet()
.forEach(
entry -> {
try {
String name = entry.getKey();
Class<? extends Hello> bindingClass =
environment
.classLoader()
.loadClass(entry.getValue().toString())
.asSubclass(Hello.class);
bind(Hello.class).annotatedWith(Names.named(name)).to(bindingClass);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
}
});
}
}
注意: ほとんどの場合、コンポーネントを作成するときに
Config
にアクセスする必要がある場合は、Config
オブジェクトをコンポーネント自体またはコンポーネントのProvider
にインジェクトする必要があります。その後、コンポーネントを作成するときにConfig
を読み取ることができます。通常、コンポーネントのバインディングを作成するときにConfig
を読み取る必要はありません。
§先行バインディング
上記のコードでは、新しいEnglishHello
オブジェクトとGermanHello
オブジェクトは、使用されるたびに作成されます。これらのオブジェクトを一度だけ作成したい場合、たとえば作成にコストがかかる場合は、@Singleton
アノテーションを使用する必要があります。オブジェクトを一度作成し、必要なときに遅延して作成するのではなく、アプリケーションの起動時に_先行して_作成する場合は、Guiceの先行シングルトンバインディングを使用できます。
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
// A Module is needed to register bindings
public class Module extends AbstractModule {
protected void configure() {
// Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class).asEagerSingleton();
bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class).asEagerSingleton();
}
}
先行シングルトンは、アプリケーションの起動時にサービスを起動するために使用できます。 シャットダウンフックと組み合わせて使用されることが多く、アプリケーションの停止時にサービスがリソースをクリーンアップできるようにします。
import javax.inject.*;
import play.inject.ApplicationLifecycle;
import play.Environment;
import java.util.concurrent.CompletableFuture;
// This creates an `ApplicationStart` object once at start-up.
@Singleton
public class ApplicationStart {
// Inject the application's Environment upon start-up and register hook(s) for shut-down.
@Inject
public ApplicationStart(ApplicationLifecycle lifecycle, Environment environment) {
// Shut-down hook
lifecycle.addStopHook(
() -> {
return CompletableFuture.completedFuture(null);
});
// ...
}
}
import com.google.inject.AbstractModule;
public class StartModule extends AbstractModule {
protected void configure() {
bind(ApplicationStart.class).asEagerSingleton();
}
}
§Playライブラリ
Playのライブラリを実装している場合は、アプリケーションで使用されているDIフレームワークに関係なく、ライブラリがそのまま使用できるように、DIフレームワークに依存しないようにするのが適切です。このため、PlayはDIフレームワークに依存しない方法でバインディングを提供するための軽量バインディングAPIを提供します.
バインディングを提供するには、Moduleを実装して、提供するバインディングのシーケンスを返します。 Module
トレイトは、バインディングを構築するためのDSLも提供します。
import com.typesafe.config.Config;
import java.util.Arrays;
import java.util.List;
import play.Environment;
import play.inject.Binding;
import play.inject.Module;
public class HelloModule extends Module {
@Override
public List<Binding<?>> bindings(Environment environment, Config config) {
return Arrays.asList(
bindClass(Hello.class).qualifiedWith("en").to(EnglishHello.class),
bindClass(Hello.class).qualifiedWith("de").to(GermanHello.class));
}
}
このモジュールは、reference.conf
のplay.modules.enabled
リストに追加することで、Playに自動的に登録できます。
play.modules.enabled += "com.example.HelloModule"
Module
の`bindings`メソッドは、Playの`Environment`と`Configuration`を受け取ります。 バインディングを動的に設定する場合は、これらにアクセスできます。- モジュールバインディングは先行バインディングをサポートしています。先行バインディングを宣言するには、`Binding`の最後に`.eagerly()`を追加します。
フレームワーク間の互換性を最大限に高めるために、以下の点に留意してください。
- すべてのDIフレームワークがジャストインタイムバインディングをサポートしているわけではありません。ライブラリが提供するすべてのコンポーネントが明示的にバインドされていることを確認してください。
- バインディングキーはシンプルにしてください。実行時のDIフレームワークによって、キーとは何か、どのように一意であるべきかについて、非常に異なる見解があります。
§モジュールの除外
ロードしたくないモジュールがある場合は、application.conf
のplay.modules.disabled
プロパティに追加することで除外できます。
play.modules.disabled += "play.api.db.evolutions.EvolutionsModule"
§循環依存関係の管理
循環依存関係は、あるコンポーネントが、元のコンポーネントに依存する(直接または間接的に)別のコンポーネントに依存する場合に発生します。例えば
public class Foo {
@Inject
public Foo(Bar bar) {
// ...
}
}
public class Bar {
@Inject
public Bar(Baz baz) {
// ...
}
}
public class Baz {
@Inject
public Baz(Foo foo) {
// ...
}
}
この場合、Foo
はBar
に依存し、Bar
はBaz
に依存し、Baz
はFoo
に依存します。そのため、これらのクラスのいずれもインスタンス化できません。 Provider
を使用することで、この問題を回避できます。
public class Foo {
@Inject
public Foo(Bar bar) {
// ...
}
}
public class Bar {
@Inject
public Bar(Baz baz) {
// ...
}
}
public class Baz {
@Inject
public Baz(Provider<Foo> fooProvider) {
// ...
}
}
コンストラクターインジェクションを使用している場合、コンポーネントを手動でインスタンス化できないため、循環依存関係がある場合ははるかに明確になります。
一般に、循環依存関係は、コンポーネントをより原子的に分割するか、依存するより具体的なコンポーネントを見つけることで解決できます。一般的な問題は、Application
への依存関係です。コンポーネントがApplication
に依存している場合、そのジョブを実行するには完全なアプリケーションが必要であることを示しています。通常はそうではありません。依存関係は、必要な特定の機能を持つ、より具体的なコンポーネント(例:Environment
)にする必要があります。最後の手段として、Provider<Application>
をインジェクトすることで問題を回避できます。
§上級編:GuiceApplicationLoaderの拡張
Playの実行時依存性注入は、GuiceApplicationLoader
クラスによってブートストラップされます。このクラスはすべてのモジュールをロードし、モジュールをGuiceにフィードし、Guiceを使用してアプリケーションを作成します。Guiceがアプリケーションを初期化する方法を制御したい場合は、GuiceApplicationLoader
クラスを拡張できます。
オーバーライドできるメソッドはいくつかありますが、通常は`builder`メソッドをオーバーライドします。このメソッドは`ApplicationLoader.Context`を読み取り、`GuiceApplicationBuilder`を作成します。以下に、`builder`の標準実装を示します。これは、好きなように変更できます。 `GuiceApplicationBuilder`の使用方法については、Guiceを使用したテストに関するセクションを参照してください。
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import play.ApplicationLoader;
import play.inject.guice.GuiceApplicationBuilder;
import play.inject.guice.GuiceApplicationLoader;
public class CustomApplicationLoader extends GuiceApplicationLoader {
@Override
public GuiceApplicationBuilder builder(ApplicationLoader.Context context) {
Config extra = ConfigFactory.parseString("a = 1");
return initialBuilder
.in(context.environment())
.loadConfig(extra.withFallback(context.initialConfig()))
.overrides(overrides(context));
}
}
ApplicationLoader
をオーバーライドする場合は、Playに指示する必要があります。 application.conf
に次の設定を追加します。
play.application.loader = "modules.CustomApplicationLoader"
依存性注入にGuiceを使用することに限定されません。 ApplicationLoader
をオーバーライドすることで、アプリケーションの初期化方法を制御できます。
§サブクラスに触れずにクラスに依存関係を追加する
多くのサブクラスを持つ可能性のある基底クラスに新しい依存関係を追加したい場合があります。
依存関係をそれぞれに直接提供することを避けるために、インジェクション可能なフィールドとして追加できます。
このアプローチは、クラスのテスト容易性を低下させる可能性があるため、注意して使用してください。
import com.google.inject.ImplementedBy;
@ImplementedBy(LiveCounter.class)
interface Counter {
public void inc(String label);
}
public class NoopCounter implements Counter {
public void inc(String label) {}
}
import javax.inject.Singleton;
@Singleton
public class LiveCounter implements Counter {
public void inc(String label) {
System.out.println("inc " + label);
}
}
import javax.inject.Inject;
import play.mvc.Controller;
import play.mvc.Result;
public class BaseController extends Controller {
// LiveCounter will be injected
@Inject protected volatile Counter counter = new NoopCounter();
public Result someBaseAction(String source) {
counter.inc(source);
return ok(source);
}
}
import javax.inject.Singleton;
import play.mvc.Result;
@Singleton
public class SubclassController extends BaseController {
public Result index() {
return someBaseAction("index");
}
}
次へ:コンパイル時依存性注入
このドキュメントに誤りがありましたか?このページのソースコードはこちらにあります。 ドキュメントガイドラインを読んだ後、プルリクエストを送信してください。質問やアドバイスがあれば、コミュニティフォーラムにアクセスして、コミュニティとの会話を始めてください。