Dependency injection using Google Guice framework

Guice is a lightweight Java framework for handling dependency injection. It makes code modular, loosely coupled, and easy to develop, debug, maintain, and test. It even won the 18th Jolt Award for best Library, Framework, or Component.

Ivan Polovyi
Javarevisited
23 min readMar 15, 2023

--

The best approach to learning something is using practical examples. Hence this tutorial will be based on a simple project. The code will be pushed to a GitHub repository and the link will be presented at the end of this tutorial. Each chapter will be saved as a separate package, so you can keep track of progressions made during this tutorial.

V1. Challenge and initial project

Let's imagine a situation where we have a task to create a simple system that generates reports about customers for example in CSV format. The class diagram will be like below:

We create a simple class like below:

package com.polovyi.ivan.tutorials.v1;

public class CSVReportGenerator {

public String generate() {
return "Customer CSV report!";
}
}

It has one method which generates a report, actually, it just returns a simple string but never the less it is enough for our example. Now we can use this class from another to generate a report like below:

package com.polovyi.ivan.tutorials.v1;

public class CustomerService {

private CSVReportGenerator csvReportGenerator;

public CustomerService(CSVReportGenerator csvReportGenerator) {
this.csvReportGenerator = csvReportGenerator;
}

public void generateCustomerReport() {
String report = csvReportGenerator.generate();
System.out.println("report = " + report);
}
}

And we have a client who actually makes a call to generate a report:

package com.polovyi.ivan.tutorials.v1;

public class Client {

public static void main(String[] args) {
CSVReportGenerator csvReportGenerator = new CSVReportGenerator();
CustomerService customerService = new CustomerService(csvReportGenerator);
customerService.generateCustomerReport();
}
}

Now the class CustomerService becomes dependent on a CustomerCSVReportService class. It is dependent because without it won't be able to generate a report. And the class CustomerCSVReportService becomes a dependency of CustomerService class.

The problem with this code is that the CustomerService class is tightly coupled with its dependency. And if in the future we will need to generate the same type of report but in a different format, for example, XML, we will need to modify the customer CustomerService class.

V2. Adding an interface to a project

To make our CustomerService class more generic and loosely coupled we transform our code as follows:

As you can see from a diagram we create an interface for the report class, with only one method:

package com.polovyi.ivan.tutorials.v2;

public interface ReportGenerator {

String generate();
}

Then we implement this interface in the report class:

package com.polovyi.ivan.tutorials.v2;

public class CSVReportGenerator implements ReportGenerator {

@Override
public String generate() {
return "Customer CSV report!";
}
}

And now we can modify our CustomerService class, such as the variable will be of interface type.

package com.polovyi.ivan.tutorials.v2;

public class CustomerService {

private ReportGenerator reportGenerator;

public CustomerService(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

So now the above class can accept int its constructor any object of a class that implements the ReportGenerator interface hence our code becomes more flexible. And the client will look like below:

package com.polovyi.ivan.tutorials.v2;

public class Client {

public static void main(String[] args) {
ReportGenerator reportGenerator = new CSVReportGenerator();
CustomerService customerService = new CustomerService(reportGenerator);
customerService.generateCustomerReport();
}
}

V3. Adding Guise to the project

But we still have to create the dependency class in the client. It would be nice to have a tool that could do it for us. This is where the Google Guice comes into the picture. So the project will look like this:

To start using Guice we have to add a maven dependency to the project. At the time of writing this tutorial, the latest version is 5.1.0. And the latest version can be found in the Maven repository.

In a very simplified way, the Guice can be thought of as a map of objects and their keys. First, we have to fill up this map with objects that we want Guice to handle, and then we need to specify places in the code where we want to use those objects.

To tell Guice what object it has to manage (add to the map) we have to create a special class, that extends the AbstractModule.

package com.polovyi.ivan.tutorials.v3;

import com.google.inject.AbstractModule;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {
bind(ReportGenerator.class).to(CSVReportGenerator.class);
}
}

Then we have to override the method called configure. In this method, we bind the interface to the interface implementation. We do this because this class will actually create the dependency instance on our behalf. It will play a role of an injector. That's why it has to know what implementation to create for a given interface. And then the client looks like below:

package com.polovyi.ivan.tutorials.v3;

import com.google.inject.Guice;
import com.google.inject.Injector;


public class Client {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new ReportGeneratorModule());
ReportGenerator reportGenerator = injector.getInstance(CSVReportGenerator.class);
CustomerService customerServiceForCSV = new CustomerService(reportGenerator);
customerServiceForCSV.generateCustomerReport();
}
}

Here we create an instance of an injector from the previously created module and use it to instantiate a necessary dependency class.

V4. Better use of an Injector. @Inject annotation usages

We can simplify it even more, instead of creating an instance of CustomerService class we can annotate its constructor with @Inject annotation and ask the Guice injector to create the instance of the class for us:

package com.polovyi.ivan.tutorials.v4;

import com.google.inject.Inject;

public class CustomerService {

private ReportGenerator reportGenerator;

@Inject
public CustomerService(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

The client will look like this now:

package com.polovyi.ivan.tutorials.v4;

import com.google.inject.Guice;
import com.google.inject.Injector;


public class Client {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new ReportGeneratorModule());
CustomerService customerServiceForCSV = injector.getInstance(CustomerService.class);
customerServiceForCSV.generateCustomerReport();
}
}

Behind the scene, when the injector will create the instance of the CustomerService class it will do it by calling a constructor. Because the constructor is annotated with @Inject annotation the injector will proceed with the creation of the object that is a parameter of a constructor. Injector will check the bindings in the module class to figure out what type of implementation class it has to use to create that object. That is why we created it and added binding. With this information, the injector creates an object and passes it as a parameter to a CustomerService constructor, and creates a needed instance.

There are three ways that an injector can create an object:

  1. When an object is needed to be created from a concrete class, then the injector creates it without additional binding, because it doesn't have any ambiguity it knows what class it will use to create an object.
  2. When the object has to be created from a class that implements an interface, in this case, we have to create a binding like in the previous example
  3. The same approach as the previous is used when we need to create an object from a subclass, in this situation we have to add binding to the module class.

The @Inject annotation can be used on a constructor, field, and setter method. The best way is to use the constructor because it is the more natural way and because two other approaches rely on reflection, hence I highly recommend you use it on the constructor.

V5. @Named annotation usage for binding conflict injections

Let's say we have another requirement to create a BillingService class, which will have a feature to generate a report but this time in XML format. Now the diagram of the system looks like below:

We need to create another implementation for the ReportGenerator interface to generate XML reports.

package com.polovyi.ivan.tutorials.v5;

public class XMLReportGenerator implements ReportGenerator {

@Override
public String generate() {
return "Customer XML report!";
}
}

And create the BillingService class, in the same way, we created a Customer service class:

package com.polovyi.ivan.tutorials.v5;

import com.google.inject.Inject;

public class BillingService {

private ReportGenerator reportGenerator;

@Inject
public BillingService(ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

And now we can use this class to generate reports in XML:

package com.polovyi.ivan.tutorials.v5;

import com.google.inject.Guice;
import com.google.inject.Injector;


public class Client {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new ReportGeneratorModule());

CustomerService customerServiceForCSV = injector.getInstance(CustomerService.class);
customerServiceForCSV.generateCustomerReport();

BillingService billingService = injector.getInstance(BillingService.class);
billingService.generateCustomerReport();
}
}

But wait, when we run the client we will generate two CSV reports. It happens because when Guice will create instances for each service it will check the module class and use the binding there to find the appropriate implementation for a dependency. As a result for both services, it injects the CSVReportGenerator. So now we have to tell the Guice what implementation it should use for each class.

There is a generic annotation called @Named that accepts as a parameter a name of an object. So we can use it to distinguish our objects at the injection site:

package com.polovyi.ivan.tutorials.v5;

import com.google.inject.Inject;
import com.google.inject.name.Named;

public class BillingService {

private ReportGenerator reportGenerator;

@Inject
public BillingService(@Named("XMLReportImpl") ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

And the customer service class:

package com.polovyi.ivan.tutorials.v5;

import com.google.inject.Inject;
import com.google.inject.name.Named;

public class CustomerService {

private ReportGenerator reportGenerator;

@Inject
public CustomerService(@Named("CSVReportImpl") ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

And we have to refactor a module class as well:

package com.polovyi.ivan.tutorials.v5;

import com.google.inject.AbstractModule;
import com.google.inject.name.Names;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

bind(ReportGenerator.class)
.annotatedWith(Names.named("CSVReportImpl")).to(CSVReportGenerator.class);
bind(ReportGenerator.class)
.annotatedWith(Names.named("XMLReportImpl")).to(XMLReportGenerator.class);
}
}

V6. Custom purpose-built annotations for better type safety.

The previous approach is error-prone. Imagine if you misspell the name of the object, it will give you the error only at runtime. So there is a bit different way of solving the same problem. We can create custom annotations. In this case, we create one for CSV report implementation:

package com.polovyi.ivan.tutorials.v6;

import com.google.inject.BindingAnnotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
public @interface CSVReportImpl {}

And another for XML report implementation:

package com.polovyi.ivan.tutorials.v6;

import com.google.inject.BindingAnnotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
public @interface XMLReportImpl {}

And now we can use those annotations to tell what implementation Guise should use in each case. For BillingService class:

package com.polovyi.ivan.tutorials.v6;

import com.google.inject.Inject;

public class BillingService {

private ReportGenerator reportGenerator;

@Inject
public BillingService(@XMLReportImpl ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

And for CustomerService class:

package com.polovyi.ivan.tutorials.v6;

import com.google.inject.Inject;

public class CustomerService {

private ReportGenerator reportGenerator;

@Inject
public CustomerService(@CSVReportImpl ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}

public void generateCustomerReport() {
String report = reportGenerator.generate();
System.out.println("report = " + report);
}
}

To make annotation work we have to bind those annotations to a specific implementation in the module class:

package com.polovyi.ivan.tutorials.v6;

import com.google.inject.AbstractModule;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

bind(ReportGenerator.class)
.annotatedWith(CSVReportImpl.class).to(CSVReportGenerator.class);
bind(ReportGenerator.class)
.annotatedWith(XMLReportImpl.class).to(XMLReportGenerator.class);
}
}

Now it works, perfectly.

V7. Singleton object creation using Guice

What if we need to create a singleton object? For example, to create a report we need to fetch data from a database. To connect to a DB we going to use a DAO class. In this example, the class only will mimic the database but let's pretend it is a real DAO with a connection to a database. As a good practice, such objects have to be implemented following a Singleton pattern. A Singleton pattern is applied when only one instance of a class is required across the entire application. Now the application will look like this:

A new DAO class is below:

package com.polovyi.ivan.tutorials.v7;

import java.time.LocalDate;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Data;

public class CustomerDAO {

public Set<Customer> findCustomers() {
return Set.of(
new Customer("1", "Customer1", LocalDate.now()),
new Customer("2", "Customer2", LocalDate.now().minusDays(1))

);
}

@Data
@AllArgsConstructor
public static class Customer {
private String id;
private String customerName;
private LocalDate createdAt;
}
}

It is a fake DAO class that has one method that returns a set of customers. As you can see on the diagram now each of the report generators has as a dependency an object created from CustomerDAO class. So the classes look like below:

package com.polovyi.ivan.tutorials.v7;

import com.google.inject.Inject;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;

@AllArgsConstructor(onConstructor=@__(@Inject))
public class CSVReportGenerator implements ReportGenerator {

private CustomerDAO dao;

@Override
public String generate() {
System.out.println("dao = " + dao);
return dao.findCustomers().stream()
.map(customer -> Stream.of(customer.getId(),
customer.getCustomerName(),
customer.getCreatedAt().toString())
.collect(Collectors.joining(";", "", "\r\n")))
.collect(Collectors.joining());

}
}

And the generator for XML report:

package com.polovyi.ivan.tutorials.v7;

import com.google.inject.Inject;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;

@AllArgsConstructor(onConstructor = @__(@Inject))
public class XMLReportGenerator implements ReportGenerator {

private CustomerDAO dao;

@Override
public String generate() {
System.out.println("dao = " + dao);
return "<customers>\r\n" + dao.findCustomers().stream()
.map(customer ->
"<id>" + customer.getId() + "<id/>\r\n"
+ "<customerName>" + customer.getCustomerName() + "<customer/>\r\n"
+ "<createdAt>" + customer.getCreatedAt().toString() + "<createdAt/>\r\n"
).collect(Collectors.joining("", "<customer>\r\n", "<customer/>\r\n")) +
" <customers/>";
}
}

The first point is here Im using Lombok annotation instead of a real constructor. As you already know the constructor must be annotated with @Inject annotation to let Guice know that we want to inject a dependency. The Lombok annotation allows us to specify the annotation we want to put on the constructor that Lombock will create for us. And the second point is that our class generates a report :).

And last but not least we have to add this new DAO class to the module in the same way we did before. The only difference here is that we have to specify that we want it to be a Singleton. And because this is a concrete class and not an interface we don't need to bind it to the implementation. But if we need we can follow the approach specified in the comments.

package com.polovyi.ivan.tutorials.v7;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

bind(ReportGenerator.class)
.annotatedWith(CSVReportImpl.class).to(CSVReportGenerator.class);
bind(ReportGenerator.class)
.annotatedWith(XMLReportImpl.class).to(XMLReportGenerator.class);
bind(CustomerDAO.class).in(Scopes.SINGLETON);
// bind(CustomerDAO.class).to(SomeImplementation.class).in(Scopes.SINGLETON);
}
}

We can remove the binding because it is a concrete class and just annotate the CustomerDAO class itself with a @Singleton annotation, the effect will be the same. The Guice won't have ambiguity and it will create an object from a concrete class and because a class is annotated with a @Singleton annotation it will create only one object.

The client won't change. Each generator classes have a line that prints an object to a console, I added this line just to show that in both classes the output will be the same because both will use the same object.

V8. Using @Provides annotation

Now we have another requirement. After a report is generated we have to send it via email. For this, we will use the third-party API. The API client will be imported into the project as an external jar (let's pretend).

The client class looks like below:

package com.polovyi.ivan.tutorials.v8;

public class ThirdPartyEmailAPIClient {

private ThirdPartyEmailAPIClient() {
}

// @Inject annotation can not be added
public ThirdPartyEmailAPIClient(String apiKey) {
this.apiKey = apiKey;
}

private String apiKey;

void sendEmail(String address, String report) {
System.out.println("Fake report " + report +
"successfully sent to fake address " +
address + " by fake client with api key "+ this.apiKey);
}
}

This class has one method that sends an email with a report, and it has one required constructor with an API key as a parameter. We can't modify this class and annotate it with @ Inject annotation to be able to inject this parameter. Remember that it is a class from an imported jar.

For this kind of situation, Guise provides @Provides annotation. We can create a method in the module class or another class that extends AbstractModule, annotate this method with this annotation and Guice will handle the rest.

package com.polovyi.ivan.tutorials.v8;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {
bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).toInstance("third-party-api-key");
}

@Singleton
@Provides
public ThirdPartyEmailAPIClient instantiateClient(String apiKey) {
return new ThirdPartyEmailAPIClient(apiKey);
}


@Provides
@CSVReportImpl
public ReportGenerator instantiateCSVReportGenerator(CSVReportGenerator generator) {
return generator;
}

@Provides
@XMLReportImpl
public ReportGenerator instantiateXMLReportGenerator(XMLReportGenerator generator) {
return generator;
}
}

As you can see we can use a @Singleton annotation on the method asking Guice to create a singleton object.

This example demonstrates as well that Guice handles the injection of parameters of the method annotated with the @Provides annotation method. This method has only one sting parameter, but Guice handles all parameters and parameters can be of any type.

And it is possible to inject a constant value using Guice. I bind a String class to an instance of a string with the value. The Guise will inject this string into the provider method. Use this type of binding with caution because like this it will inject all strings when a string injection is required. So it is good practice to add some qualifier annotation either generic or custom. Doing this will guarantee that the injection happens in the right place.

I refactored bindings for GenerateReport implementation so you can see that we can do basically the same with the provider method as with bindings.

And now we can use this class in our services:

package com.polovyi.ivan.tutorials.v8;

import com.google.inject.Inject;

public class CustomerService {

private ReportGenerator reportGenerator;
private ThirdPartyEmailAPIClient apiClient;

@Inject
public CustomerService(@CSVReportImpl ReportGenerator reportGenerator, ThirdPartyEmailAPIClient apiClient) {
this.reportGenerator = reportGenerator;
this.apiClient = apiClient;
}

public void generateCustomerReport() {
System.out.println("apiClient = " + apiClient);
String report = reportGenerator.generate();
System.out.println("report = " + report);
apiClient.sendEmail("fake-csv@email.com", report);
}
}

And a billing service:

package com.polovyi.ivan.tutorials.v8;

import com.google.inject.Inject;

public class BillingService {

private ReportGenerator reportGenerator;
private ThirdPartyEmailAPIClient apiClient;

@Inject
public BillingService(@XMLReportImpl ReportGenerator reportGenerator, ThirdPartyEmailAPIClient apiClient) {
this.reportGenerator = reportGenerator;
this.apiClient = apiClient;
}

public void generateCustomerReport() {
System.out.println("apiClient = " + apiClient);
String report = reportGenerator.generate();
System.out.println("report = " + report);
apiClient.sendEmail("fake-xml@email.com", report);
}
}

V9. Provider classes

Most likely in a real-life application you will have complex objects with extended instantiation processes, so you may prefer to separate object provisioning in a separate class. In this example, we will create a provider class for the instantiation of a ThirdPartyEmailAPIClient object.

package com.polovyi.ivan.tutorials.v9;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;

public class ThirdPartyEmailAPIClientProvider implements Provider<ThirdPartyEmailAPIClient> {

private final String apiKey;

@Inject
public ThirdPartyEmailAPIClientProvider(@Named("apiKey") String apiKey) {
this.apiKey = apiKey;
}

@Override
public ThirdPartyEmailAPIClient get() {
return new ThirdPartyEmailAPIClient(apiKey);
}
}

The Guice has a special interface that helps handle these cases. We create our provided class that implements a Provider interface. This interface has only one method that we have to implement. Basically, this method will provide the required object.

The good thing is that this class can have a constructor so we can inject dependency here as well. And as you can see I've corrected the way how we inject a string into the ThirdPartyEmailAPIClient class by using @Named annotation.

Now we have to register our class in the module and make it a singleton:

package com.polovyi.ivan.tutorials.v9;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.name.Names;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

bind(ReportGenerator.class)
.annotatedWith(CSVReportImpl.class).to(CSVReportGenerator.class);
bind(ReportGenerator.class)
.annotatedWith(XMLReportImpl.class).to(XMLReportGenerator.class);
bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
}
}

V10. Provider injection

There are situations where dependency is only sometimes needed. So we don't need to create an object until it is used, hence saving the resources. For this case, we can inject a provider class instead of a dependency class. Let's say in our example that the CustomerService gets an error while trying to send an email. In this scenario, we have to retry it later or/and send some message to the responsible who will take a look at the problem. For this, we can use a queue, for example, AWS SQS.

First, we have to create a queue client class:

package com.polovyi.ivan.tutorials.v10;

public class RetryQueueClient {

public void send(String message) {
System.out.println("Message " + message + "sent successfully to the queue");
}
}

Then we will create a provider class:

package com.polovyi.ivan.tutorials.v10;

import com.google.inject.Provider;

public class RetryQueueClientProvider implements Provider<RetryQueueClient> {

@Override
public RetryQueueClient get() {
return new RetryQueueClient();
}
}

After we have to add binding for this class to the module:

package com.polovyi.ivan.tutorials.v10;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.name.Names;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

bind(ReportGenerator.class)
.annotatedWith(CSVReportImpl.class).to(CSVReportGenerator.class);
bind(ReportGenerator.class)
.annotatedWith(XMLReportImpl.class).to(XMLReportGenerator.class);
bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
bind(RetryQueueClient.class).toProvider(RetryQueueClientProvider.class)
.in(Scopes.SINGLETON);
}
}

And last but not least, refactor the CustomerService class:

package com.polovyi.ivan.tutorials.v10;

import com.google.inject.Inject;

public class CustomerService {

private ReportGenerator reportGenerator;
private ThirdPartyEmailAPIClient apiClient;

private RetryQueueClientProvider retryQueueClientProvider;

@Inject
public CustomerService(@CSVReportImpl ReportGenerator reportGenerator, ThirdPartyEmailAPIClient apiClient,
RetryQueueClientProvider retryQueueClientProvider) {
this.reportGenerator = reportGenerator;
this.apiClient = apiClient;
this.retryQueueClientProvider = retryQueueClientProvider;
}

public void generateCustomerReport() {
System.out.println("apiClient = " + apiClient);
String report = reportGenerator.generate();
System.out.println("report = " + report);
try {
// if (1 == 1) {
// throw new RuntimeException();
// }
apiClient.sendEmail("fake-csv@email.com", report);
} catch (Exception e) {
System.out.println("error = " + e);
retryQueueClientProvider.get().send("Error while trying to send a report: " + report);
}
}
}

As you can see we inject here a provider directly and only in case of an error we will get the required object from RetryQueueClient class. It will be loaded lazily. To simulate an error just uncomment commented lines of code.

V11. Using multi-binder to inject a collection

It's time to make our application even more flexible. Instead of using report implementations directly in CustomerService and BillingService classes, we will use a factory class. So the class diagram of our application now looks like below:

We will modify the interface by adding the enum which will contain constants, that will indicate the type of a report. And we add one more method that will return this enum.

package com.polovyi.ivan.tutorials.v11;

public interface ReportGenerator {

String generate();

ReportType getReportType();

enum ReportType {
CSV,
XML
}
}

Now we have to modify our generators, by adding implementation for a new interface method:

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.Inject;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;

@AllArgsConstructor(onConstructor_ = @Inject)
public class CSVReportGenerator implements ReportGenerator {

private CustomerDAO dao;

@Override
public ReportType getReportType() {
return ReportType.CSV;
}

@Override
public String generate() {
System.out.println("dao = " + dao);
return dao.findCustomers().stream()
.map(customer -> Stream.of(customer.getId(),
customer.getCustomerName(),
customer.getCreatedAt().toString())
.collect(Collectors.joining(";", "", "\r\n")))
.collect(Collectors.joining());

}
}

And for XML generator:

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.Inject;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;

@AllArgsConstructor(onConstructor_ = @Inject)
public class XMLReportGenerator implements ReportGenerator {

private CustomerDAO dao;

@Override
public ReportType getReportType() {
return ReportType.XML;
}

@Override
public String generate() {
System.out.println("dao = " + dao);
return "<customers>\r\n" + dao.findCustomers().stream()
.map(customer ->
"<id>" + customer.getId() + "<id/>\r\n"
+ "<customerName>" + customer.getCustomerName() + "<customer/>\r\n"
+ "<createdAt>" + customer.getCreatedAt().toString() + "<createdAt/>\r\n"
).collect(Collectors.joining("", "<customer>\r\n", "<customer/>\r\n")) +
" <customers/>";
}
}

After we can create a factory class:

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v11.ReportGenerator.ReportType;
import java.util.Set;

public class ReportGeneratorFactory {

private final Set<ReportGenerator> reportGenerators;

@Inject
public ReportGeneratorFactory(Set<ReportGenerator> reportGenerators) {
this.reportGenerators = reportGenerators;
}

public ReportGenerator getReportGenerator(ReportType type) {
return reportGenerators.stream()
.filter(generator -> generator.getReportType().equals(type))
.findFirst().orElseThrow();
}
}

This class contains a set of generators as a variable and it is injected by Guice. As well it has a method that receives a type of report as a parameter and then uses it to pick up the right generator implementation. Now we have to tell Guice what implementation we want to bind tho that set:

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.multibindings.ProvidesIntoSet;
import com.google.inject.name.Names;
import com.polovyi.ivan.tutorials.v10.RetryQueueClient;
import com.polovyi.ivan.tutorials.v10.RetryQueueClientProvider;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {
Multibinder<ReportGenerator> reportGeneratorMultibinder = Multibinder.newSetBinder(binder(), ReportGenerator.class);
reportGeneratorMultibinder.addBinding().to(CSVReportGenerator.class);

bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
bind(RetryQueueClient.class).toProvider(RetryQueueClientProvider.class)
.in(Scopes.SINGLETON);
}

@ProvidesIntoSet
public ReportGenerator instantiateXMLReportGenerator(XMLReportGenerator generator) {
return generator;
}
}

For this case, we have to use a multi-binder and add to it all the required implementations. And if we need to add bindings from the provider method we can use @ProvidesIntoSet annotation.

Now we will modify CustomerService to use a factory method instead of directly using a report object:

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v11.ReportGenerator.ReportType;

public class CustomerService {

private ThirdPartyEmailAPIClient apiClient;
private ReportGeneratorFactory factory;
private RetryQueueClientProvider retryQueueClientProvider;

@Inject
public CustomerService(ThirdPartyEmailAPIClient apiClient,
ReportGeneratorFactory factory, RetryQueueClientProvider retryQueueClientProvider) {
this.apiClient = apiClient;
this.factory = factory;
this.retryQueueClientProvider = retryQueueClientProvider;
}

public void generateCustomerReport() {
System.out.println("apiClient = " + apiClient);
String report = factory.getReportGenerator(ReportType.CSV).generate();
System.out.println("report = " + report);
try {
// if (1 == 1) {
// throw new RuntimeException();
// }
apiClient.sendEmail("fake-csv@email.com", report);
} catch (Exception e) {
System.out.println("error = " + e);
retryQueueClientProvider.get().send("Error while trying to send a report: " + report);
}
}
}

And in billing service, we follow the same approach. The only difference is that for different services we specify different types of reports that are passed to the factory class.

package com.polovyi.ivan.tutorials.v11;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v11.ReportGenerator.ReportType;

public class BillingService {

private ThirdPartyEmailAPIClient apiClient;
private ReportGeneratorFactory factory;

@Inject
public BillingService(ThirdPartyEmailAPIClient apiClient, ReportGeneratorFactory factory) {
this.apiClient = apiClient;
this.factory = factory;
}

public void generateCustomerReport() {
System.out.println("apiClient = " + apiClient);
String report = factory.getReportGenerator(ReportType.XML).generate();
System.out.println("report = " + report);
apiClient.sendEmail("fake-xml@email.com", report);
}
}

V12. Using a map binder to inject a collection

We can achieve the same with a bit better approach by using the binding map. For this, we will refactor the module class to use map binding. Here we can use 2 approaches. First is binding in the configure method of an abstract class. And second is when we use a provider method, then we have to annotate this method with a special annotation to tell the Guice that we want to use provided object inside the map.

For this, we can use a generic annotation @MapKey if the key in the binding map is a string, and we can use a custom annotation if the kay is of some different type, for example, enum as in our example.

package com.polovyi.ivan.tutorials.v12;


import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.google.inject.multibindings.MapKey;
import java.lang.annotation.Retention;

@MapKey()
@Retention(RUNTIME)
public @interface ProvidesIntoMapKey {
ReportGenerator.ReportType value();
}

And now the module class will look like the below:

package com.polovyi.ivan.tutorials.v12;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.ProvidesIntoMap;
import com.google.inject.name.Names;
import com.polovyi.ivan.tutorials.v12.ReportGenerator.ReportType;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

MapBinder<ReportType, ReportGenerator> reportGeneratorBinder =
MapBinder.newMapBinder(binder(), ReportType.class, ReportGenerator.class);

reportGeneratorBinder.addBinding(ReportType.CSV).to(CSVReportGenerator.class);

bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
}

//@StringMapKey("XML") - if a string is used for the key
@ProvidesIntoMapKey(ReportType.XML)
@ProvidesIntoMap
public ReportGenerator instantiateXMLReportGenerator(XMLReportGenerator generator) {
return generator;
}
}

As you can see we add bindings to the map where the key is an enum and the value is an implementation. Now we have to refactor the factory class, so it uses a map instead of a set.

package com.polovyi.ivan.tutorials.v12;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v12.ReportGenerator.ReportType;
import java.util.Map;
import java.util.Optional;

public class ReportGeneratorFactory {

private final Map<ReportType, ReportGenerator> reportGenerators;

@Inject
public ReportGeneratorFactory(Map<ReportType, ReportGenerator> reportGenerators) {
this.reportGenerators = reportGenerators;
}

public ReportGenerator getReportGenerator(ReportType type) {
return Optional.ofNullable(reportGenerators.get(type))
.orElseThrow();
}
}

The rest of the application won't need any change.

V13. Optional bindings

There will be a case when the injection of optional dependency is required. For such a situation optional binding can be used.

Let's say we want to add a logger to the CustomerService and BillingService classes. And the longer will be optional if the logger object is present then it will be injected and logs will be created otherwise nothing will happen.

Now we will create a logger interface with only one method, that accepts the class and the message as parameters.

package com.polovyi.ivan.tutorials.v13;

public interface Logger {

<T> void log(Class<T> clazz, String message);
}

And after we will implement this interface:

package com.polovyi.ivan.tutorials.v13;

import java.time.LocalDateTime;

public class CustomLogger implements Logger {

public <T> void log(Class<T> clazz, String message) {
System.out.println("[" + LocalDateTime.now() + "]-[" + clazz.getName() + "]-[" + message + "]");
}
}

Then we will add this class as a dependency to CustomerService:

package com.polovyi.ivan.tutorials.v13;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v13.ReportGenerator.ReportType;
import java.util.Optional;

public class CustomerService {

private ThirdPartyEmailAPIClient apiClient;
private ReportGeneratorFactory factory;
private RetryQueueClientProvider retryQueueClientProvider;
private Optional<CustomLogger> logger;

@Inject
public CustomerService(ThirdPartyEmailAPIClient apiClient,
ReportGeneratorFactory factory,
RetryQueueClientProvider retryQueueClientProvider,
Optional<CustomLogger> logger) {
this.apiClient = apiClient;
this.factory = factory;
this.retryQueueClientProvider = retryQueueClientProvider;
this.logger = logger;
}

public void generateCustomerReport() {
logger.ifPresent(log -> log.log(this.getClass(), "apiClient = " + apiClient));
String report = factory.getReportGenerator(ReportType.CSV).generate();
logger.ifPresent(log -> log.log(this.getClass(), "report = " + report));

try {
// if (1 == 1) {
// throw new RuntimeException();
// }
apiClient.sendEmail("fake-csv@email.com", report);
} catch (Exception e) {
logger.ifPresent(log -> log.log(this.getClass(), "error = " + e.getMessage()));
retryQueueClientProvider.get().send("Error while trying to send a report: " + report);
}
}
}

and to BillingService:

package com.polovyi.ivan.tutorials.v13;

import com.google.inject.Inject;
import com.polovyi.ivan.tutorials.v13.ReportGenerator.ReportType;
import java.util.Optional;

public class BillingService {

private ThirdPartyEmailAPIClient apiClient;
private ReportGeneratorFactory factory;
private Optional<CustomLogger> logger;

@Inject
public BillingService(ThirdPartyEmailAPIClient apiClient,
ReportGeneratorFactory factory,
Optional<CustomLogger> logger) {
this.apiClient = apiClient;
this.factory = factory;
this.logger = logger;
}
public void generateCustomerReport() {
logger.ifPresent(log -> log.log(this.getClass(), "apiClient = " + apiClient));
String report = factory.getReportGenerator(ReportType.XML).generate();
logger.ifPresent(log -> log.log(this.getClass(), "report = " + report));
apiClient.sendEmail("fake-xml@email.com", report);
}
}

As you can see if this dependency is injected we will log some messages if not, nothing will happen.

Last but not least we need to modify a module class by adding a provider class and optional binding.

package com.polovyi.ivan.tutorials.v13;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.OptionalBinder;
import com.google.inject.name.Names;
import com.polovyi.ivan.tutorials.v13.ReportGenerator.ReportType;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

MapBinder<ReportType, ReportGenerator> reportGeneratorBinder =
MapBinder.newMapBinder(binder(), ReportType.class, ReportGenerator.class);

reportGeneratorBinder.addBinding(ReportType.CSV).to(CSVReportGenerator.class);
reportGeneratorBinder.addBinding(ReportType.XML).to(XMLReportGenerator.class);

bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
bind(RetryQueueClient.class).toProvider(RetryQueueClientProvider.class)
.in(Scopes.SINGLETON);

OptionalBinder.newOptionalBinder(binder(), CustomLogger.class);
}

@Singleton
@Provides
public Logger provideLogger(CustomLogger logger) {
return logger;
}
}

When we run the code we will see the logs o the console. If we remove a provider method and run the code nothing will be logged.

V14. Optional injection

There is a way to optionally inject a dependency when it exists and use a default one when it does not.

Imagine our DAO class needs a database URL injected. We can modify a DAO class:

package com.polovyi.ivan.tutorials.v14;

import com.google.inject.Inject;
import com.google.inject.name.Named;
import java.time.LocalDate;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Data;

public class CustomerDAO {

private static final String URL = "http://localhost";
private String url = URL;

@Inject(optional = true)
public void setUrl(@Named("DatabaseUrl") String url) {
this.url = url;
}

public Set<Customer> findCustomers() {
System.out.println("Fetching data from DB using url = " + url);
return Set.of(
new Customer("1", "Customer1", LocalDate.now()),
new Customer("2", "Customer2", LocalDate.now().minusDays(1))

);
}

@Data
@AllArgsConstructor
public static class Customer {

private String id;
private String customerName;
private LocalDate createdAt;
}
}

We add a default URL here and use a set method for injection value for this variable from outside the class. Now we have to add bindings in the module class:

package com.polovyi.ivan.tutorials.v14;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Scopes;
import com.google.inject.Singleton;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.OptionalBinder;
import com.google.inject.name.Names;
import com.polovyi.ivan.tutorials.v14.ReportGenerator.ReportType;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

MapBinder<ReportType, ReportGenerator> reportGeneratorBinder =
MapBinder.newMapBinder(binder(), ReportType.class, ReportGenerator.class);

reportGeneratorBinder.addBinding(ReportType.CSV).to(CSVReportGenerator.class);
reportGeneratorBinder.addBinding(ReportType.XML).to(XMLReportGenerator.class);

bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
bind(RetryQueueClient.class).toProvider(RetryQueueClientProvider.class)
.in(Scopes.SINGLETON);
OptionalBinder.newOptionalBinder(binder(), CustomLogger.class);
bind(String.class).annotatedWith(Names.named("DatabaseUrl")).toInstance("http://url-from-configuration");

}

@Singleton
@Provides
public Logger provideLogger(CustomLogger logger) {
return logger;
}
}

When we remove bindings from the module class the DAO class will use the default value. It is worth mentioning that the preferable way of handling optional dependency is optional binding described in the previous chapter.

V15. Module arrangement

In the end, our application has a large module class. Guice lets us have multiple modules, so we can split a large volume class into smaller ones, and organize them better. We can have multiple or nested modules. Let's create a separate module for optional bindings:

package com.polovyi.ivan.tutorials.v15;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.multibindings.OptionalBinder;

public class OptionalBinderModule extends AbstractModule {

@Override
protected void configure() {
OptionalBinder.newOptionalBinder(binder(), CustomLogger.class);
}

@Singleton
@Provides
public Logger provideLogger(CustomLogger logger) {
return logger;
}
}

And one separate for collection bindings:

package com.polovyi.ivan.tutorials.v15;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.name.Names;
import com.polovyi.ivan.tutorials.v15.ReportGenerator.ReportType;

public class CollectionBinderModule extends AbstractModule {

@Override
protected void configure() {
MapBinder<ReportType, ReportGenerator> reportGeneratorBinder =
MapBinder.newMapBinder(binder(), ReportType.class, ReportGenerator.class);

reportGeneratorBinder.addBinding(ReportType.CSV).to(CSVReportGenerator.class);
reportGeneratorBinder.addBinding(ReportType.XML).to(XMLReportGenerator.class);
}
}

Now we have to refactor the previous module class, by taking out bindings that were moved to other modules.

package com.polovyi.ivan.tutorials.v15;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.name.Names;

public class ReportGeneratorModule extends AbstractModule {

@Override
protected void configure() {

install(new CollectionBinderModule());

bind(CustomerDAO.class).in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("apiKey")).toInstance("third-party-api-key");
bind(ThirdPartyEmailAPIClient.class).toProvider(ThirdPartyEmailAPIClientProvider.class)
.in(Scopes.SINGLETON);
bind(RetryQueueClient.class).toProvider(RetryQueueClientProvider.class)
.in(Scopes.SINGLETON);
bind(String.class).annotatedWith(Names.named("DatabaseUrl")).toInstance("http://url-from-configuration");
}
}

When we want to nest one module in another we can use the install method in the configure method of a module. In our example, the CollectionBinderModule is nested inside the ReportGeneratorModule, and we don't need to instantiate it separately.

Last but not least we have to refactor a client:

package com.polovyi.ivan.tutorials.v15;

import com.google.inject.Guice;
import com.google.inject.Injector;


public class Client {

public static void main(String[] args) {
Injector injector = Guice.createInjector(new ReportGeneratorModule(), new OptionalBinderModule());

CustomerService customerServiceForCSV = injector.getInstance(CustomerService.class);
customerServiceForCSV.generateCustomerReport();

BillingService billingService = injector.getInstance(BillingService.class);
billingService.generateCustomerReport();
}
}

We have to add a new OptionalBinderModule to the injector creator method. CollectionBinderModule we don't need to specify it here because it is a nested module of ReportGeneratorModule and it will be registered automatically.

The complete code can be found here:

Conclusion

Guice is a very powerful and easy-to-use tool that every developer should know. Understanding the internals of Guice will help to work with any dependency injection framework out there. The complete documentation can be found here.

Thank you for reading! Please like and follow. If you have any questions or suggestions, please feel free to write in the comments section or on my LinkedIn account.

Become a member for full access to Medium content.

--

--

Ivan Polovyi
Javarevisited

I am a Java Developer | OCA Java EE 8 | Spring Professional | AWS CDA | CKA | DCA | Oracle DB CA. I started programming at age 34 and still learning.