Introduction à l'inversion du contrôle et à l'injection de dépendances avec Spring

1. Vue d'ensemble

Dans cet article, nous présenterons les concepts d'IoC (Inversion of Control) et DI (Dependency Injection), puis nous examinerons comment ils sont implémentés dans le framework Spring.

2. Qu'est-ce que l'inversion de contrôle?

L'inversion de contrôle est un principe en génie logiciel par lequel le contrôle d'objets ou de parties d'un programme est transféré vers un conteneur ou un framework. Il est le plus souvent utilisé dans le contexte de la programmation orientée objet.

Contrairement à la programmation traditionnelle, dans laquelle notre code personnalisé appelle une bibliothèque, IoC permet à un framework de prendre le contrôle du flux d'un programme et de faire des appels à notre code personnalisé. Pour permettre cela, les frameworks utilisent des abstractions avec un comportement supplémentaire intégré. Si nous voulons ajouter notre propre comportement, nous devons étendre les classes du framework ou plugin nos propres classes.

Les avantages de cette architecture sont:

  • découpler l'exécution d'une tâche de sa mise en œuvre
  • facilitant le basculement entre les différentes implémentations
  • une plus grande modularité d'un programme
  • une plus grande facilité à tester un programme en isolant un composant ou en se moquant de ses dépendances et en permettant aux composants de communiquer via des contrats

L'inversion de contrôle peut être obtenue grâce à divers mécanismes tels que: modèle de conception de stratégie, modèle de localisateur de service, modèle d'usine et injection de dépendance (DI).

Nous allons examiner DI ensuite.

3. Qu'est-ce que l'injection de dépendance?

L'injection de dépendances est un modèle par lequel implémenter l'IoC, où le contrôle inversé est le paramètre des dépendances d'objet.

L'acte de connecter des objets avec d'autres objets, ou «d'injecter» des objets dans d'autres objets, est fait par un assembleur plutôt que par les objets eux-mêmes.

Voici comment créer une dépendance d'objet dans la programmation traditionnelle:

public class Store { private Item item; public Store() { item = new ItemImpl1(); } }

Dans l'exemple ci-dessus, nous devons instancier une implémentation de l' interface Item dans la classe Store elle-même.

En utilisant DI, nous pouvons réécrire l'exemple sans spécifier l'implémentation de Item que nous voulons:

public class Store { private Item item; public Store(Item item) { this.item = item; } }

Dans les sections suivantes, nous verrons comment nous pouvons fournir l'implémentation de Item via des métadonnées.

L'IoC et la DI sont des concepts simples, mais ont des implications profondes dans la façon dont nous structurons nos systèmes, ils méritent donc d'être bien compris.

4. Le conteneur Spring IoC

Un conteneur IoC est une caractéristique commune des frameworks qui implémentent l'IoC.

Dans le framework Spring, le conteneur IoC est représenté par l'interface ApplicationContext . Le conteneur Spring est responsable de l'instanciation, de la configuration et de l'assemblage des objets appelés beans , ainsi que de la gestion de leur cycle de vie.

Le framework Spring fournit plusieurs implémentations de l' interface ApplicationContext - ClassPathXmlApplicationContext et FileSystemXmlApplicationContext pour les applications autonomes et WebApplicationContext pour les applications Web.

Pour assembler des beans, le conteneur utilise des métadonnées de configuration, qui peuvent être sous la forme de configuration XML ou d'annotations.

Voici une façon d'instancier manuellement un conteneur:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

Pour définir l' attribut d' élément dans l'exemple ci-dessus, nous pouvons utiliser des métadonnées. Ensuite, le conteneur lira ces métadonnées et les utilisera pour assembler des beans lors de l'exécution.

L'injection de dépendances dans Spring peut être effectuée via des constructeurs, des setters ou des champs.

5. Injection de dépendances basée sur le constructeur

Dans le cas d'une injection de dépendances basée sur un constructeur, le conteneur appellera un constructeur avec des arguments représentant chacun une dépendance que nous voulons définir.

Spring résout chaque argument principalement par type, suivi du nom de l'attribut et de l'index pour la clarification. Voyons la configuration d'un bean et ses dépendances à l'aide d'annotations:

@Configuration public class AppConfig { @Bean public Item item1() { return new ItemImpl1(); } @Bean public Store store() { return new Store(item1()); } }

L' annotation @Configuration indique que la classe est une source de définitions de bean. De plus, nous pouvons l'ajouter à plusieurs classes de configuration.

L' annotation @Bean est utilisée sur une méthode pour définir un bean. Si nous ne spécifions pas de nom personnalisé, le nom du bean sera par défaut le nom de la méthode.

Pour un bean avec la portée singleton par défaut , Spring vérifie d'abord si une instance mise en cache du bean existe déjà et n'en crée une nouvelle que si ce n'est pas le cas. Si nous utilisons la portée prototype , le conteneur retourne une nouvelle instance de bean pour chaque appel de méthode.

Une autre façon de créer la configuration des beans est la configuration XML:

6. Injection de dépendances basée sur un poseur

Pour l'ID basée sur un setter, le conteneur appellera les méthodes setter de notre classe, après avoir appelé un constructeur sans argument ou une méthode de fabrique statique sans argument pour instancier le bean. Créons cette configuration à l'aide d'annotations:

@Bean public Store store() { Store store = new Store(); store.setItem(item1()); return store; }

Nous pouvons également utiliser XML pour la même configuration de beans:

Constructor-based and setter-based types of injection can be combined for the same bean. The Spring documentation recommends using constructor-based injection for mandatory dependencies, and setter-based injection for optional ones.

7. Field-Based Dependency Injection

In case of Field-Based DI, we can inject the dependencies by marking them with an @Autowired annotation:

public class Store { @Autowired private Item item; }

While constructing the Store object, if there's no constructor or setter method to inject the Item bean, the container will use reflection to inject Item into Store.

We can also achieve this using XML configuration.

This approach might look simpler and cleaner but is not recommended to use because it has a few drawbacks such as:

  • This method uses reflection to inject the dependencies, which is costlier than constructor-based or setter-based injection
  • It's really easy to keep adding multiple dependencies using this approach. If you were using constructor injection having multiple arguments would have made us think that the class does more than one thing which can violate the Single Responsibility Principle.

More information on @Autowired annotation can be found in Wiring In Spring article.

8. Autowiring Dependencies

Wiring allows the Spring container to automatically resolve dependencies between collaborating beans by inspecting the beans that have been defined.

There are four modes of autowiring a bean using an XML configuration:

  • no: the default value – this means no autowiring is used for the bean and we have to explicitly name the dependencies
  • byName: autowiring is done based on the name of the property, therefore Spring will look for a bean with the same name as the property that needs to be set
  • byType: similar to the byName autowiring, only based on the type of the property. This means Spring will look for a bean with the same type of the property to set. If there's more than one bean of that type, the framework throws an exception.
  • constructor: autowiring is done based on constructor arguments, meaning Spring will look for beans with the same type as the constructor arguments

For example, let's autowire the item1 bean defined above by type into the store bean:

@Bean(autowire = Autowire.BY_TYPE) public class Store { private Item item; public setItem(Item item){ this.item = item; } }

We can also inject beans using the @Autowired annotation for autowiring by type:

public class Store { @Autowired private Item item; }

If there's more than one bean of the same type, we can use the @Qualifier annotation to reference a bean by name:

public class Store { @Autowired @Qualifier("item1") private Item item; }

Now, let's autowire beans by type through XML configuration:

Next, let's inject a bean named item into the item property of store bean by name through XML:

We can also override the autowiring by defining dependencies explicitly through constructor arguments or setters.

9. Lazy Initialized Beans

By default, the container creates and configures all singleton beans during initialization. To avoid this, you can use the lazy-init attribute with value true on the bean configuration:

As a consequence, the item1 bean will be initialized only when it's first requested, and not at startup. The advantage of this is faster initialization time, but the trade-off is that configuration errors may be discovered only after the bean is requested, which could be several hours or even days after the application has already been running.

10. Conclusion

In this article, we've presented the concepts of inversion of control and dependency injection and exemplified them in the Spring framework.

You can read more about these concepts in Martin Fowler's articles:

  • Inversion des conteneurs de contrôle et du modèle d'injection de dépendances.
  • Inversion de contrôle

Et vous pouvez en savoir plus sur les implémentations Spring d'IoC et DI dans la documentation de référence Spring Framework.