Guice vs Spring - Injection de dépendance

1. Introduction

Google Guice et Spring sont deux frameworks robustes utilisés pour l'injection de dépendances. Les deux frameworks couvrent toutes les notions d'injection de dépendances, mais chacun a sa propre manière de les implémenter.

Dans ce didacticiel, nous verrons comment les frameworks Guice et Spring diffèrent en termes de configuration et d'implémentation.

2. Dépendances de Maven

Commençons par ajouter les dépendances Guice et Spring Maven dans notre fichier pom.xml :

 org.springframework spring-context 5.1.4.RELEASE   com.google.inject guice 4.2.2 

Nous pouvons toujours accéder aux dernières dépendances spring-context ou guice depuis Maven Central.

3. Configuration d'injection de dépendance

L'injection de dépendances est une technique de programmation que nous utilisons pour rendre nos classes indépendantes de leurs dépendances.

Dans cette section, nous ferons référence à plusieurs fonctionnalités de base qui diffèrent entre Spring et Guice dans leur manière de configurer l'injection de dépendances.

3.1. Câblage à ressort

Spring déclare les configurations d'injection de dépendances dans une classe de configuration spéciale. Cette classe doit être annotée par l' annotation @Configuration . Le conteneur Spring utilise cette classe comme source de définitions de bean.

Les classes gérées par Spring sont appelées Spring beans.

Spring utilise l' annotation @Autowired pour câbler automatiquement les dépendances . @Autowired fait partie des annotations de base intégrées de Spring. Nous pouvons utiliser @Autowired sur les variables membres, les méthodes setter et les constructeurs.

Spring prend également en charge @Inject. @Inject fait partie du CDI Java (Contexts and Dependency Injection) qui définit un standard pour l'injection de dépendances.

Disons que nous voulons câbler automatiquement une dépendance à une variable membre. Nous pouvons simplement l'annoter avec @Autowired :

@Component public class UserService { @Autowired private AccountService accountService; }
@Component public class AccountServiceImpl implements AccountService { }

Deuxièmement, créons une classe de configuration à utiliser comme source de beans lors du chargement de notre contexte d'application:

@Configuration @ComponentScan("com.baeldung.di.spring") public class SpringMainConfig { }

Notez que nous avons également annotés UserService et AccountServiceImpl avec @component pour les enregistrer comme des haricots. C'est l' annotation @ComponentScan qui indiquera à Spring où rechercher les composants annotés.

Même si nous avons annoté AccountServiceImpl , Spring peut le mapper au AccountService car il implémente AccountService .

Ensuite, nous devons définir un contexte d'application pour accéder aux beans. Notons simplement que nous ferons référence à ce contexte dans tous nos tests unitaires Spring:

ApplicationContext context = new AnnotationConfigApplicationContext(SpringMainConfig.class);

Désormais, au moment de l'exécution, nous pouvons récupérer l' instance A ccountService à partir de notre bean UserService :

UserService userService = context.getBean(UserService.class); assertNotNull(userService.getAccountService());

3.2. Reliure de guice

Guice gère ses dépendances dans une classe spéciale appelée module. Un module Guice doit étendre la classe AbstractModule et remplacer sa méthode configure () .

Guice utilise la liaison comme l'équivalent du câblage dans Spring. En termes simples, les liaisons nous permettent de définir comment les dépendances vont être injectées dans une classe . Les liaisons de guice sont déclarées dans la méthode configure () de notre module .

Au lieu de @Autowired , Guice utilise l' annotation @Inject pour injecter les dépendances.

Créons un exemple Guice équivalent:

public class GuiceUserService { @Inject private AccountService accountService; }

Deuxièmement, nous allons créer la classe de module qui est une source de nos définitions de liaison:

public class GuiceModule extends AbstractModule { @Override protected void configure() { bind(AccountService.class).to(AccountServiceImpl.class); } }

Normalement, nous nous attendons à ce que Guice instancie chaque objet de dépendance à partir de leurs constructeurs par défaut s'il n'y a pas de liaison définie explicitement dans la méthode configure () . Mais comme les interfaces ne peuvent pas être instanciées directement, nous devons définir des liaisons pour indiquer à Guice quelle interface sera associée à quelle implémentation.

Ensuite, nous devons définir un injecteur à l' aide de GuiceModule pour obtenir des instances de nos classes. Notons simplement que tous nos tests Guice utiliseront cet injecteur :

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

Enfin, au moment de l'exécution, nous récupérons une instance GuiceUserService avec une dépendance accountService non nulle :

GuiceUserService guiceUserService = injector.getInstance(GuiceUserService.class); assertNotNull(guiceUserService.getAccountService());

3.3. Annotation @Bean de Spring

Spring fournit également une annotation de niveau méthode @Bean pour enregistrer les beans comme alternative à ses annotations de niveau classe comme @Component . La valeur de retour d'une méthode annotée @Bean est enregistrée en tant que bean dans le conteneur.

Disons que nous avons une instance de BookServiceImpl que nous voulons rendre disponible pour injection. Nous pourrions utiliser @Bean pour enregistrer notre instance:

@Bean public BookService bookServiceGenerator() { return new BookServiceImpl(); }

Et maintenant, nous pouvons obtenir un bean BookService :

BookService bookService = context.getBean(BookService.class); assertNotNull(bookService);

3.4. Guice's @Provides Annotation

As an equivalent of Spring's @Bean annotation, Guice has a built-in annotation @Provides to do the same job. Like @Bean, @Provides is only applied to the methods.

Now let's implement the previous Spring bean example with Guice. All we need to do is to add the following code into our module class:

@Provides public BookService bookServiceGenerator() { return new BookServiceImpl(); }

And now, we can retrieve an instance of BookService:

BookService bookService = injector.getInstance(BookService.class); assertNotNull(bookService);

3.5. Classpath Component Scanning in Spring

Spring provides a @ComponentScan annotation detects and instantiates annotated components automatically by scanning pre-defined packages.

The @ComponentScan annotation tells Spring which packages will be scanned for annotated components. It is used with @Configuration annotation.

3.6. Classpath Component Scanning in Guice

Unlike Spring, Guice doesn't have such a component scanning feature. But it's not difficult to simulate it. There are some plugins like Governator that can bring this feature into Guice.

3.7. Object Recognition in Spring

Spring recognizes objects by their names. Spring holds the objects in a structure which is roughly like a Map. This means that we cannot have two objects with the same name.

Bean collision due to having multiple beans of the same name is one common problem Spring developers hit. For example, let's consider the following bean declarations:

@Configuration @Import({SpringBeansConfig.class}) @ComponentScan("com.baeldung.di.spring") public class SpringMainConfig { @Bean public BookService bookServiceGenerator() { return new BookServiceImpl(); } }
@Configuration public class SpringBeansConfig { @Bean public AudioBookService bookServiceGenerator() { return new AudioBookServiceImpl(); } }

As we remember, we already had a bean definition for BookService in SpringMainConfig class.

To create a bean collision here, we need to declare the bean methods with the same name. But we are not allowed to have two different methods with the same name in one class. For that reason, we declared the AudioBookService bean in another configuration class.

Now, let's refer these beans in a unit test:

BookService bookService = context.getBean(BookService.class); assertNotNull(bookService); AudioBookService audioBookService = context.getBean(AudioBookService.class); assertNotNull(audioBookService);

The unit test will fail with:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'AudioBookService' available

First, Spring registered the AudioBookService bean with “bookServiceGenerator” name in its bean map. Then, it had to override it by the bean definition for BookService due to the “no duplicate names allowed” nature of the HashMap data structure.

Lastly, we can overcome this issue by making bean method names unique or setting the name attribute to a unique name for each @Bean.

3.8. Object Recognition in Guice

Unlike Spring, Guice basically has a Map structure . This means that we cannot have multiple bindings to the same type without using additional metadata.

Guice provides binding annotations to enable defining multiple bindings for the same type. Let's see what happens if we have two different bindings for the same type in Guice.

public class Person { }

Now, let's declare two different binding for the Person class:

bind(Person.class).toConstructor(Person.class.getConstructor()); bind(Person.class).toProvider(new Provider() { public Person get() { Person p = new Person(); return p; } });

And here is how we can get an instance of Person class:

Person person = injector.getInstance(Person.class); assertNotNull(person);

This will fail with:

com.google.inject.CreationException: A binding to Person was already configured at GuiceModule.configure()

We can overcome this issue by just simply discarding one of the bindings for the Person class.

3.9. Optional Dependencies in Spring

Optional dependencies are dependencies which are not required when autowiring or injecting beans.

For a field that has been annotated with @Autowired, if a bean with matching data type is not found in the context, Spring will throw NoSuchBeanDefinitionException.

However, sometimes we may want to skip autowiring for some dependencies and leave them as nullwithout throwing an exception:

Now let's take a look at the following example:

@Component public class BookServiceImpl implements BookService { @Autowired private AuthorService authorService; }
public class AuthorServiceImpl implements AuthorService { }

As we can see from the code above, AuthorServiceImpl class hasn't been annotated as a component. And we'll assume that there isn't a bean declaration method for it in our configuration files.

Now, let's run the following test to see what happens:

BookService bookService = context.getBean(BookService.class); assertNotNull(bookService);

Not surprisingly, it will fail with:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'AuthorService' available

We can make authorService dependency optional by using Java 8's Optional type to avoid this exception.

public class BookServiceImpl implements BookService { @Autowired private Optional authorService; }

Now, our authorService dependency is more like a container that may or may not contain a bean of AuthorService type. Even though there isn't a bean for AuthorService in our application context, our authorService field will still be non-null empty container. Hence, Spring won't have any reason to throw NoSuchBeanDefinitionException.

As an alternative to Optional, we can use @Autowired‘s required attribute, which is set to true by default, to make a dependency optional. We can set the required attribute to false to make a dependency optional for autowiring.

Hence, Spring will skip injecting the dependency if a bean for its data type is not available in the context. The dependency will remain set to null:

@Component public class BookServiceImpl implements BookService { @Autowired(required = false) private AuthorService authorService; }

Sometimes marking dependencies optional can be useful since not all the dependencies are always required.

With this in mind, we should remember that we'll need to use extra caution and null-checks during development to avoid any NullPointerException due to the null dependencies.

3.10. Optional Dependencies in Guice

Just like Spring, Guice can also use Java 8's Optional type to make a dependency optional.

Let's say that we want to create a class and with a Foo dependency:

public class FooProcessor { @Inject private Foo foo; }

Now, let's define a binding for the Foo class:

bind(Foo.class).toProvider(new Provider() { public Foo get() { return null; } });

Now let's try to get an instance of FooProcessor in a unit test:

FooProcessor fooProcessor = injector.getInstance(FooProcessor.class); assertNotNull(fooProcessor);

Our unit test will fail with:

com.google.inject.ProvisionException: null returned by binding at GuiceModule.configure(..) but the 1st parameter of FooProcessor.[...] is not @Nullable

In order to skip this exception, we can make the foo dependency optional with a simple update:

public class FooProcessor { @Inject private Optional foo; }

@Inject doesn't have a required attribute to mark the dependency optional. An alternative approach to make a dependency optional in Guice is to use the @Nullable annotation.

Guice tolerates injecting null values in case of using @Nullable as expressed in the exception message above. Let's apply the @Nullable annotation:

public class FooProcessor { @Inject @Nullable private Foo foo; }

4. Implementations of Dependency Injection Types

In this section, we'll take a look at the dependency injection types and compare the implementations provided by Spring and Guice by going through several examples.

4.1. Constructor Injection in Spring

In constructor-based dependency injection, we pass the required dependencies into a class at the time of instantiation.

Let's say that we want to have a Spring component and we want to add dependencies through its constructor. We can annotate that constructor with @Autowired:

@Component public class SpringPersonService { private PersonDao personDao; @Autowired public SpringPersonService(PersonDao personDao) { this.personDao = personDao; } }

Starting with Spring 4, the @Autowired dependency is not required for this type of injection if the class has only one constructor.

Let's retrieve a SpringPersonService bean in a test:

SpringPersonService personService = context.getBean(SpringPersonService.class); assertNotNull(personService);

4.2. Constructor Injection in Guice

We can rearrange the previous example to implement constructor injection in Guice. Note that Guice uses @Inject instead of @Autowired.

public class GuicePersonService { private PersonDao personDao; @Inject public GuicePersonService(PersonDao personDao) { this.personDao = personDao; } }

Here is how we can get an instance of GuicePersonService class from the injector in a test:

GuicePersonService personService = injector.getInstance(GuicePersonService.class); assertNotNull(personService);

4.3. Setter or Method Injection in Spring

In setter-based dependency injection, the container will call setter methods of the class, after invoking the constructor to instantiate the component.

Let's say that we want Spring to autowire a dependency using a setter method. We can annotate that setter method with @Autowired:

@Component public class SpringPersonService { private PersonDao personDao; @Autowired public void setPersonDao(PersonDao personDao) { this.personDao = personDao; } }

Whenever we need an instance of SpringPersonService class, Spring will autowire the personDao field by invoking the setPersonDao() method.

We can get a SpringPersonService bean and access its personDao field in a test as below:

SpringPersonService personService = context.getBean(SpringPersonService.class); assertNotNull(personService); assertNotNull(personService.getPersonDao());

4.4. Setter or Method Injection in Guice

We'll simply change our example a bit to achieve setter injection in Guice.

public class GuicePersonService { private PersonDao personDao; @Inject public void setPersonDao(PersonDao personDao) { this.personDao = personDao; } }

Every time we get an instance of GuicePersonService class from the injector, we'll have the personDao field passed to the setter method above.

Here is how we can create an instance of GuicePersonService class and access its personDao fieldin a test:

GuicePersonService personService = injector.getInstance(GuicePersonService.class); assertNotNull(personService); assertNotNull(personService.getPersonDao());

4.5. Field Injection in Spring

Nous avons déjà vu comment appliquer l'injection de champ à la fois pour Spring et Guice dans tous nos exemples. Ce n'est donc pas un nouveau concept pour nous. Mais listons-le simplement pour être complet.

Dans le cas de l'injection de dépendances basée sur un champ, nous injectons les dépendances en les marquant avec @Autowired ou @Inject .

4.6. Injection de champ à Guice

Comme nous l'avons mentionné dans la section ci-dessus, nous avons déjà couvert l' injection de champ pour Guice en utilisant @Inject .

5. Conclusion

Dans ce didacticiel, nous avons exploré les différentes différences fondamentales entre les frameworks Guice et Spring dans leur manière d'implémenter l'injection de dépendances. Comme toujours, les exemples de code Guice et Spring sont terminés sur GitHub.