Guice全能:AssistedInject、マルチバインディング、ジェネリック

最近、私はGuiceDIフレームワークとして使用しているチームと頻繁に会い始めました。 私はそれを恐れました(愛する春を降ろします!?)、そして、それは私の人生で通常起こるように、私の恐怖が具体化しました-私はGuiceが積極的に使用しているプロジェクトに乗りました...



かなりの量の出版物(ロシア語の出版物を含む)がすでにこのフレームワークでインターネットに蓄積されています。これは朗報です。 しかし、プロジェクトでは、既成のソリューションを見つけることができない状況に直面しました。



この記事では、 Guiceとその拡張機能の実際の使用方法を再度示します: AssistedInjectMutiBinding 、およびGenericsの使用 。 最初に問題の本質を説明し、次にその解決策を繰り返し説明します。 私は読者にフレームワークと一般的なDIについての基本的なアイデアの存在を意味するので、基本を省略します。 さらに、優れたドキュメントがあります



プロジェクトのソースコードとその反復の履歴はgithubで見つけることができます。



1.リクエストハンドラー



状況は次のとおりです。 このリクエストのエグゼキューターを作成するために必要なパラメーターと、エグゼキューターがその後の作業に使用する引数を使用して、リクエストが(例えばRESTを介して)到着すると想像してください。



public class Request { public String parameter; public int argument; }
      
      





多くのワーカーエグゼクティブ(階層全体)がおり、それぞれが作業を完了するためにサービスの形で依存関係を必要とし、依存関係のセットはエグゼキューターによって異なる場合があります。 これを、抽象クラスと2つの子孫からの単純な階層の例と考えてください。 実際、もちろん、これはどのNでも機能するはずです



したがって、 Workerプロトタイプ:



 public abstract class Worker{ protected final int argument; public Worker(int argument) { this.argument = argument; } public abstract void doWork(); }
      
      





ワーカー実装自体:



 public class Worker1 extends Worker { private ServiceA serviceA; private ServiceB serviceB; public Worker1(ServiceA serviceA, ServiceB serviceB, int argument) { super(argument); this.serviceA = serviceA; this.serviceB = serviceB; } @Override public void doWork() { System.out.println(String.format("Worker1 starts work with argument %d services %s and %s", argument, serviceA, serviceB)); } } public class Worker2 extends Worker { private ServiceB serviceB; private ServiceC serviceC; public Worker2(ServiceB serviceB, ServiceC serviceC, int argument) { super(argument); this.serviceB = serviceB; this.serviceC = serviceC; } @Override public void doWork() { System.out.println(String.format("Worker2 starts work with argument %d services %s and %s", argument, serviceB, serviceC)); } }
      
      





シンプルなハンドラー実装



ハンドラーコードを初めて見たときは、次のようになりました。



 public class RequestHandler { private final ServiceA serviceA; private final ServiceB serviceB; private final ServiceC serviceC; public RequestHandler(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC) { this.serviceA = serviceA; this.serviceB = serviceB; this.serviceC = serviceC; } public void handleRequest(Request request) { Worker worker = null; if (request.parameter.equals("case1")) { worker = new Worker1(request.argument); } else if (request.parameter.equals("case2")) { worker = new Worker2(request.argument); } //          //,     worker.setServiceA(serviceA); worker.setServiceB(serviceB); worker.setServiceC(serviceC); worker.doWork(); } }
      
      





なぜこのアプローチが悪いのですか? このコードの短所の短いリストを作成しようとします。





私自身は、このようにまとめました。 ワーカーGuiceに転送したいのです!



2. Guiceの接続



まず、 Maven依存関係をpom.xmlに追加します。



 <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>${guice.version}</version> </dependency>
      
      





執筆時点でのGuiceの最新バージョンは4.2.0です。



繰り返し移動することを約束したので、最初はタスクを単純化します。 Requestに引数はありません。 Workerは、サービスの形でいくつかの依存関係を持つ非常に単純なクラスです。 つまり 抽象クラスは非常に簡単です。



 public abstract class Worker { public abstract void doWork(); }
      
      





そして、その実装は次のようになります( Worker2の実装は似ています):



 public class Worker1 extends Worker{ private ServiceA serviceA; private ServiceB serviceB; @Inject public Worker1(ServiceA serviceA, ServiceB serviceB) { this.serviceA = serviceA; this.serviceB = serviceB; } @Override public void doWork() { System.out.println(String.format("Worker1 starts work with %s and %s", serviceA, serviceB)); } }
      
      





この場合の@ Injectアノテーションは、 Workerのインスタンスを作成するには、このアノテーションでマークされたコンストラクターを使用し、作成時にすべての入力パラメーターをコンストラクターに提供する必要があることをフレームワークに伝えます。 サービスをどこで手に入れるかは気にしないかもしれませんが、 Guiceがすべてをしてくれます。



RequestHandlerは次のようになります。



 @Singleton public class RequestHandler { private Provider<Worker1> worker1Provider; private Provider<Worker2> worker2Provider; @Inject public RequestHandler(Provider<Worker1> worker1Provider, Provider<Worker2> worker2Provider) { this.worker1Provider = worker1Provider; this.worker2Provider = worker2Provider; } public void handleRequest(Request request) { Worker worker = null; if (request.parameter.equals("case1")) { worker = worker1Provider.get(); } else if (request.parameter.equals("case2")) { worker = worker2Provider.get(); } worker.doWork(); } }
      
      





このクラスのサービスへの依存関係を取り除いたことがすぐにわかります。 代わりに、 Workerによって入力されたプロバイダーが注入されました。 ドキュメントから:

プロバイダー<T>-タイプTのインスタンスを提供できるオブジェクト
この場合、 プロバイダーGuiceフレームワークによって提供されるファクトリーです。 Workerクラスによって入力されたプロバイダーへの依存関係を取得した後、 .get()



メソッドを呼び出すたびに、 Workerクラスの新しいインスタンスを取得します(もちろん、 WorkerSingletonとして宣言されていない限り)。



RequestHandlerは、順番に@ Singletonアノテーションでマークされていることに注意してください。 これは、 Guiceがアプリケーションにこのクラスのインスタンスが2 つないことを確認することを意味します。



コードを実行します。



  public static void main( String[] args ) { Request request = new Request(); request.parameter = "case1"; request.argument = 5; Injector injector = Guice.createInjector(); RequestHandler requestHandler = injector.getInstance(RequestHandler.class); requestHandler.handleRequest(request); request.parameter = "case2"; requestHandler.handleRequest(request); }
      
      





実行結果
Worker1はServiceAとServiceBで作業を開始します

Worker2はServiceBとServiceCで作業を開始します



3.引数を投げる



そして、元の形式のWorkerクラスに戻ります。 これを行うには、コンストラクターに新しい引数パラメーターを渡します。



 @Inject public Worker1(ServiceA serviceA, ServiceB serviceB, int argument) {
      
      





問題は、アノテーションがある場合、@ Inject Guiceがコンストラクターで指定されたすべてのパラメーターを提供するため、 Runtimeで形成されたパラメーターを渡すのが難しくなることです。



もちろん、独自のFactoryを作成することでこの問題を解決できます。



労働者工場
 @Singleton public class WorkerFactory { private ServiceA serviceA; private ServiceB serviceB; private ServiceC serviceC; @Inject public WorkerFactory(ServiceA serviceA, ServiceB serviceB, ServiceC serviceC) { this.serviceA = serviceA; this.serviceB = serviceB; this.serviceC = serviceC; } public Worker1 createWorker1 (int argument) { return new Worker1(serviceA, serviceB, argument); } public Worker2 createWorker2 (int argument) { return new Worker2(serviceB, serviceC, argument); } }
      
      





上記のコードを正確に実行すると、同じ結果が表示されます。



このようなファクトリーには、非常に多くの定型コードが含まれています。作成されたクラスで使用されるすべての依存関係を明示的に指定し、それらを注入し、 new演算子を使用して呼び出してコンストラクターに明示的に渡す必要があります。 Guiceは、その拡張機能によってこの雑用を回避します。



Guice AssistedInject



拡張機能Guiceの拡張機能に接続します。



 <dependency> <groupId>com.google.inject.extensions</groupId> <artifactId>guice-assistedinject</artifactId> <version>${guice.version}</version> </dependency>
      
      





ここで、大きなWorkerFactoryクラスを作成する代わりに、同じ名前のインターフェイスを作成します。



 public interface WorkerFactory { Worker1 createWorker1 (int argument); Worker2 createWorker2 (int argument); }
      
      





インターフェイスの実装は記述しません。Guiceが行います。 Moduleを使用してこれを構成します。



 public class Module extends AbstractModule { @Override protected void configure() { install(new FactoryModuleBuilder().implement(Worker1.class, Worker1.class) .implement(Worker2.class, Worker2.class) .build(WorkerFactory.class)); } }
      
      





モジュールは、 Guiceヘルパー構成のクラスとして見ることができます。 Injectorの作成時に複数のモジュールを接続することもできます。また、モジュールをモジュールに接続することもできます。これにより、柔軟でカスタマイズ可能で読みやすい構成システムを作成できます。



ファクトリーを作成するために、 FactoryModuleBuilderを使用しました 。 ドキュメントから:

FactoryModuleBuilder-呼び出し元の引数とインジェクターが提供する値を組み合わせてオブジェクトを構築するファクトリーを提供します。
Guiceが提供するオブジェクトとカスタムパラメータを組み合わせる機会があります。



ファクトリーの作成をさらに詳しく分析しましょう:





ワーカーコンストラクターのどのパラメーターがファクトリーを介してロールバックされ 、どのパラメーターがティアリングフレームワークに残されるかについて、 Guiceに伝えることを忘れないでください。 @ Assistedアノテーションを使用してこれを行います。



 @AssistedInject public Worker1(ServiceA serviceA, ServiceB serviceB, @Assisted int argument)
      
      





@ Assistedアノテーションは、私たち自身がファクトリーからGuiceに提供する引数の上に置かれます。 また、通常、この場合、コンストラクターで@ AssistedInjectは @ Injectに置き換えられます。



RequestHandlerを書き換えて、 WorkerFactoryに依存関係を追加します。



 @Singleton public class RequestHandler { private WorkerFactory workerFactory; @Inject public RequestHandler(WorkerFactory workerFactory) { this.workerFactory = workerFactory; } public void handleRequest(Request request) { Worker worker = null; if (request.parameter.equals("case1")) { worker = workerFactory.createWorker1(request.argument); } else if (request.parameter.equals("case2")) { worker = workerFactory.createWorker2(request.argument); } worker.doWork(); } }
      
      





最後にもう1つ触れました。コンテキストを上げるために、 Guiceはモジュールについて調べる必要があります。 モジュールを指定するだけで、 Injectorを取得するために変更はありません。



  Injector injector = Guice.createInjector(new Module());
      
      





実行結果
Worker1は、引数5サービスServiceAおよびServiceBで作業を開始します

Worker2は、引数5サービスServiceBおよびServiceCで作業を開始します



4.ファクトリーのパラメーター化



新しいWorker後継が表示されるたびに、それをWorkerFactoryインターフェースに追加し、これをModuleに報告する必要があるのは本当ですか?

WorkerWorkerFactoryをパラメーター化することでこれを取り除こうとし、同時にGuiceがこれをどのように扱うかを見つけましょう。



 public interface WorkerFactory<T extends Worker> { T createWorker (int argument); }
      
      





ここで、 Guiceフレームワークに、 ワーカーごとに1つずつ、2つの異なるファクトリインスタンスを作成する必要があることを伝える必要があります。 工場をタイプする方法だけですか? 結局のところ、 Javaはそのような構造の記述を許可しません: WorkerFactory <Worker1> .class



 public class Module extends AbstractModule{ @Override protected void configure() { install(new FactoryModuleBuilder().implement(Worker.class, Worker1.class) .build(new TypeLiteral<WorkerFactory<Worker1>>() {})); install(new FactoryModuleBuilder().implement(Worker.class, Worker2.class) .build(new TypeLiteral<WorkerFactory<Worker2>>() {})); } }
      
      





今回は、 implementメソッドの引数で、そのシグネチャに必要なものを示すことができます。Workerは抽象クラス、親、 Worker1またはWorker2はその子孫であり、対応するファクトリによって作成されます。



TypeLiteralクラスを使用してジェネリックに関する問題を解決しました。 Guiceのドキュメントから:

TypeLiteral <T>-ジェネリック型Tを表します。Javaはジェネリック型を表現する方法をまだ提供していないため、このクラスは
したがって、 Javaにはパラメーター化されたクラスに関する知識がないため、 Guiceは独自のクラスを作成しました。



通常、 Class <T>引数の代わりにTypeLiteral <T>を使用できます。オーバーロードされたメソッドを見るだけです。 TypeLiteralを作成するときは、コンストラクターがprotectedとして宣言されているため、 {}を忘れずに入れてください。



ファクトリの依存関係をRequestHandlerに接続する方法を見てみましょう:



 @Singleton public class RequestHandler { private WorkerFactory<Worker1> worker1Factory; private WorkerFactory<Worker2> worker2Factory; @Inject public RequestHandler(WorkerFactory<Worker1> worker1Factory, WorkerFactory<Worker2> worker2Factory) { this.worker1Factory = worker1Factory; this.worker2Factory = worker2Factory; } public void handleRequest(Request request) { Worker worker = null; if (request.parameter.equals("case1")) { worker = worker1Factory.createWorker(request.argument); } else if (request.parameter.equals("case2")) { worker = worker2Factory.createWorker(request.argument); } worker.doWork(); } }
      
      





5.マルチバインディング



そのため、 WorkerFactoryのパラメーター化を行い 、すべてのファクトリーに単一のインターフェイスを残しました。Workerクラスの新しい子孫を追加するときに展開する必要はありません。 ただし、代わりに、 WorkerFactory<WorkerN> workerNFactory



毎回WorkerFactory<WorkerN> workerNFactory



ファクトリに新しい依存関係を注入する必要があります。 次に、 multibindings拡張機能を使用して、これを修正します。 特に、 MapBinderを使用します

MapBinder-複数のマップエントリを個別にバインドし、後で完全なマップとして挿入するためのAPI。
MapBinderを使用すると、すべての依存関係を1つのマップにまとめて収集し、一度に注入できます



multibinings拡張機能をプロジェクトに接続します。



 <dependency> <groupId>com.google.inject.extensions</groupId> <artifactId>guice-multibindings</artifactId> <version>4.2.0</version> </dependency>
      
      





そしてすぐにモジュールを追加します-すべての魔法はその中で起こります。 まず、 MapBinderを作成します



 MapBinder<String, WorkerFactory> binder = MapBinder.newMapBinder(binder(), String.class, WorkerFactory.class);
      
      





特別なことはありません。マッピングの種類を指定するだけです。String型のパラメーターを目的のWorkerFactoryファクトリに一致させます。 マッピング自体を実装することは残っています。



そのため、 Guiceは 、私たちの助けを借りて、 Workerの工場を既に作成しました。



  new TypeLiteral<WorkerFactory<Worker1>>(){}
      
      





引数を同じオブジェクトにマッピングします。 これを行うには、 addBinding()



およびto()



メソッドを使用します。 TypeLiteralを受け入れるメソッドのオーバーロードバージョンがあることに注意してください。 したがって、モジュールは完全に表示されます。



 public class Module extends AbstractModule{ @Override protected void configure() { install(new FactoryModuleBuilder().implement(Worker.class, Worker1.class) .build(new TypeLiteral<WorkerFactory<Worker1>>() {})); install(new FactoryModuleBuilder().implement(Worker.class, Worker2.class) .build(new TypeLiteral<WorkerFactory<Worker2>>() {})); MapBinder<String, WorkerFactory> binder = MapBinder.newMapBinder(binder(), String.class, WorkerFactory.class); binder.addBinding("case1").to(new TypeLiteral<WorkerFactory<Worker1>>(){}); binder.addBinding("case2").to(new TypeLiteral<WorkerFactory<Worker2>>(){}); } }
      
      





最も興味深いことはすべて既に行われています。RequestHandlerで必要なオブジェクトを使用してMapを取得するだけです。



 @Singleton public class RequestHandler { private Map<String, WorkerFactory> workerFactoryMap; @Inject public RequestHandler(Map<String, WorkerFactory> workerFactoryMap) { this.workerFactoryMap = workerFactoryMap; } public void handleRequest(Request request) { Worker worker = workerFactoryMap.get(request.parameter) .createWorker(request.argument); worker.doWork(); } }
      
      





ご覧のとおり、依存関係を使用して@ Injectマップを実行し、 get()



メソッドで目的のファクトリーを取得します。



それだけです! RequestHandlerは Workerの作成と実行のみを担当し、すべてのマッピングはモジュールに転送されました。 新しい労働者相続人の出現で、他の何も変更せずに、そこにそれに関する情報を追加する必要があります。



少し結論



一般的に、 Guiceはそのシンプルさと、今では「低しきい値」と言うのがファッショナブルであるため、私を楽しませてくれました。 多くの場合、単純なアプリケーションでは構成をまったくスキップして、@ Injectアノテーションで対応できます。 詳細については、githubwikiをご覧ください。



All Articles