📜 ⬆️ ⬇️

Guice Almighty: assistedinject, multibindings, generics

Recently, teams that use Guice as a DI framework have become more frequent. He began to be afraid of him (get off his beloved Spring !?), And, as is usually the case in life, my fears materialized - I got on a project that actively uses Guice ...

The Internet has already accumulated a fair amount of publications (including Russian-speaking) on ​​this framework, which is good news. However, on the project I was faced with a situation, a ready solution for which I could not find.

In the article, I will once again show the practical use of Guice and some of its extensions : assistedinject , mutibindings , and also work with generics . First, I will describe the essence of the problem, and then iteratively come to its solution. I imply that the reader has a basic understanding of the framework and of DI as a whole, therefore I will omit the basics. Moreover, there is excellent documentation .
')
The source code of the project and the history of its iterations can be found on the githab .

1. Request Handler


The situation is as follows. Imagine that a request arrives for us (for example, by REST ) with the parameter, on the basis of which you want to create an executor of this request, and with an argument that will be used by the executor for his further work.

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

We have a lot of Worker executors (an entire hierarchy) and each needs dependencies in the form of services to perform its work, and the sets of dependencies may differ for different performers. Consider this on the example of a simple hierarchy of an abstract class and two heirs. In fact, of course, this should work for any N.

In line with this, the prototype Worker :

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

The Worker implementations themselves:

 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)); } } 

Simple handler implementation


When I first saw the handler code, it looked something like this:

 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(); } } 

Why is this approach bad? I will try to form a short list of the disadvantages of this code:


For myself, I summed up this: I want to translate the Worker 's to Guice !

2. Connecting Guice


First of all, add the Maven dependency to pom.xml :

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

The latest version of Guice at the time of this writing is 4.2.0.

I promised to move iteratively, so for the beginning we will simplify the task. Let us have no arguments in Request . Worker is an extremely simple class with a couple of dependencies in the form of services. Those. abstract class is extremely simple:

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

And its implementations look like this (the implementation of Worker2 looks similar):

 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)); } } 

The @Inject annotation in this case tells the framework that to create a Worker instance, you need to use the constructor marked with this annotation, and also, during creation, provide the constructor with all input parameters. We can not care about where to get the services, Guice will do everything for us.

RequestHandler will look like this:

 @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(); } } 

Immediately striking that we got rid of dependencies on services in this class. Instead, an Injectable Provider typed by Worker . From the documentation:
Provider <T> - an object capable of providing instances of type T
In this case, the Provider is a factory provided by the Guice framework. After a dependency is obtained on a provider typed by the Worker class, each time the .get() method is .get() we get a new instance of the Worker class (unless, of course, the Worker is declared as a Singleton ).

Notice that RequestHandler , in turn, is just marked with the @ Singleton annotation. This means Guice will make sure that we don’t have two instances of this class in the application.

Run the code:

  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); } 

Execution result
Worker1 starts work with ServiceA and ServiceB
Worker2 starts work with ServiceB and ServiceC

3. throwing arguments


Now let's return the original view of the Worker classes. To do this, pass a new argument parameter to the constructor:

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

The problem is that if there is an annotation, @ Inject Guice will provide all the parameters specified in the constructor, which makes it difficult to pass a parameter that is formed in Runtime .

Of course, you can solve this problem by creating your own Factory :

Worker 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); } } 

Run the code in the same way as shown above and see the same result.

Such factories contain quite a lot of template code: you must explicitly specify all the dependencies that will be used in the created class, inject them and explicitly transfer them to the constructor, calling it using the new operator. Guice avoids this chore with its extensions.

Guice AssistedInject


We connect dependence on expansion ( extension ) for Guice :

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

Now, instead of writing a large class WorkerFactory , we are doing an interface with the same name:

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

We will not write interface implementation, Guice will do it for us! We configure this using the 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)); } } 

You can look at the module as a class for supporting Guice configuration. It is possible to connect several modules when creating an Injector , and it is also possible to connect modules to modules, which makes it possible to create a flexible, customizable and readable system of configurations.

To create a factory, we used FactoryModuleBuilder . From the documentation:
FactoryModuleBuilder - provides a number of construct objects.
We have the opportunity to combine user options with objects provided by Guice .

Let us examine the creation of the factory more:


It is imperative not to forget to tell Guice about which parameters in the Worker constructor will be passed through the factory, and which parameters are left to the framework to be torn apart. We do this using the @ Assisted annotation:

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

The @ Assisted annotation is placed above the arguments that we ourselves will provide to Guice from the factory. Also, usually in this case, @ AssistedInject is put over the constructor instead of @ Inject .

Rewrite RequestHandler , adding a dependency to it on the 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(); } } 

The final touch remains - to raise the context, Guice needs to learn about our module. Nothing changes, just to get the Injector we specify the module:

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

Execution result
Worker1 starts with 5 services ServiceA and ServiceB
Worker2 starts with 5 services ServiceB and ServiceC

4. Parameterize Factory


Really, every time when we have a new successor, the Worker will have to add it to the WorkerFactory interface, and report it to the Module ?
Let's try to get rid of this by making the WorkerFactory parameterized by Worker , and at the same time we will find out how Guice does it.

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

Now you need to specify the Guice framework that you need to create two different factories of the factories - one for each Worker . Just how to make the factory typed? After all, Java does not allow writing such constructs: 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>>() {})); } } 

This time, in the arguments of the implement method, we can indicate what its signature required: Worker is an abstract class, parent, and Worker1 or Worker2 are its heirs, which will be created by the corresponding factory.

We solved the problem with generics using the TypeLiteral class. From the Guice documentation:
TypeLiteral <T> - represents a generic type of T. Java doesn’t
Thus, since Java has no idea of ​​a parameterized class, Guice created its own.

Usually, instead of the Class <T> argument, you can use TypeLiteral <T> , just look at the overloaded methods. Do not forget to set {} when creating a TypeLiteral , since its constructor is declared as protected .

Now let's see how to connect the dependencies of factories to 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. Multibindings


So, we have parameterized the WorkerFactory , leaving a single interface for all factories, which will not have to be expanded when adding new heirs of the Worker class. But instead, you will need to implement a new dependency on the WorkerFactory<WorkerN> workerNFactory every time in WorkerFactory<WorkerN> workerNFactory . Now let's fix this using the multibindings extension. In particular, we will use MapBinder :
MapBinder - an API for bind multiple maps.
MapBinder allows you to collect all the dependencies together in one map, and then inject it all at once.

We connect the multibinings extension to the project:

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

And immediately go to finish the Module - all the magic happens in it. First, create a MapBinder :

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

Nothing special, just specify the mapping types: set the String request parameter to the desired WorkerFactory factory. It remains to implement the mapping itself.

So, with our help, Guice has already created a factory for Worker :

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

Build an argument on the same object. To do this, we use the addBinding() and to() methods. Note the presence of an overloaded version of the method that accepts TypeLiteral . This is how the module will look like:

 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>>(){}); } } 

All the most interesting has already happened, it remains only to get the Map with the objects we need in RequestHandler :

 @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(); } } 

As you can see, we just do @ Inject mapy with dependencies, and then we get the necessary factory through the get() method.

And that's it! Now RequestHandler is responsible only for creating and launching the Worker , and all the mapping is transferred to the module. When new heirs appear, the Worker will need to add information about this to the same place, without changing anything else.

Small conclusion


In general, I will say that Guice pleasantly surprised me due to its simplicity and, as is now fashionable to say, “low threshold of entry”. Often, in simple applications, you can not write a configuration at all and get by with one annotation @ Inject . For more information, read the wiki on github .

Source: https://habr.com/ru/post/358278/


All Articles