Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 16

Inversion de contrôle et injection de dépendances avec Spring

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons initialisé notre premier projet Spring Boot. Nous avons maintenant une structure de projet Maven fonctionnelle, avec un point d'entrée (@SpringBootApplication) et un fichier de configuration (application.properties). Ce squelette est prêt à être étoffé.

Aujourd'hui, nous allons aborder l'un des concepts les plus fondamentaux et puissants de Spring : l'inversion de contrôle (IoC) et l'injection de dépendances (DI). L'objectif est de vous apprendre à implémenter ces principes avec les annotations @Component, @Service et @Autowired.

Si vous avez déjà utilisé Laravel, le concept de Service Container qui gère et injecte automatiquement les classes vous est familier. L'IoC de Spring est une implémentation très robuste de cette même idée. Comprendre ce mécanisme est essentiel pour construire des applications modulaires, testables et faciles à maintenir, ce qui est au cœur des bonnes pratiques de développement modernes que vous souhaitez maîtriser.

1. Le Problème : Le Couplage Fort

Imaginons une application où un service (NotificationService) dépend d'un autre (EmailService). En Java "classique", on serait tenté d'écrire ceci :

// Mauvaise pratique : couplage fort
class NotificationService {
    private EmailService emailService = new EmailService(); // Dépendance directe !

    public void notifyUser(String message) {
        emailService.sendEmail(message);
    }
}

Ici, NotificationService est fortement couplé à EmailService. L'utilisation de new crée un lien rigide entre les deux classes. Pourquoi est-ce un problème ?

  • Flexibilité réduite : Si demain nous voulons envoyer des notifications par SMS plutôt que par email, nous devons modifier le code de NotificationService pour utiliser new SMSService().
  • Difficulté de test : Il est impossible de tester NotificationService sans tester également EmailService. On ne peut pas facilement "bouchonner" (ou mocker) la dépendance pour isoler le composant à tester.

Ce couplage fort rend l'application fragile et difficile à faire évoluer.

Pour bien visualiser ce problème, la vidéo suivante l'illustre avec un exemple très parlant de couplage fort.

Part 1 Mise en oeuvre de l'injection des dépendances

Dans cette vidéo de la chaîne du Professeur Mohamed YOUSSFI, l'auteur explique de manière très claire la différence entre le couplage fort et le couplage faible, qui est le problème que l'injection de dépendances vient résoudre.

Regardez le segment de 09:22 à 20:51. L'auteur utilise un exemple avec une classe Metier qui dépend d'une classe DAO (Data Access Object). Concentrez-vous sur la démonstration de pourquoi le fait d'instancier directement un objet avec new rend l'application 'ouverte à la modification', ce qui est une mauvaise chose pour la maintenance.


2. La Solution : Dépendre d'Abstractions (Interfaces)

La solution à ce problème est un principe de conception logicielle que vous rencontrerez souvent : le Principe d'Inversion des Dépendances (DIP), le "D" de SOLID. Il stipule que les modules de haut niveau ne doivent pas dépendre de modules de bas niveau, mais que les deux doivent dépendre d'abstractions (des interfaces en Java).

Reprenons notre exemple. Au lieu de dépendre directement de EmailService, NotificationService va dépendre d'une interface MessageService.

Principe d'Inversion des Dépendances (DIP) avec le Framework Spring

Cet article du blog dev.to, "Principe d'Inversion des Dépendances (DIP) avec le Framework Spring", illustre parfaitement la transition d'un code fortement couplé vers un code faiblement couplé grâce aux interfaces.

Lisez attentivement les sections "1. Sans DIP (Code Fortement Couplé)" et "2. Application du DIP avec Spring (Code Faiblement Couplé)". Comparez les deux approches. Notez l'introduction de l'interface MessageService et comment les classes EmailService et SMSService l'implémentent. C'est le changement clé.

En dépendant de l'interface MessageService, notre NotificationService devient agnostique de l'implémentation. Il peut fonctionner avec un EmailService, un SMSService, ou toute autre classe qui respecte le contrat défini par l'interface. On parle alors de couplage faible.

Spring Dependency Diagram
Dans une application bien conçue, les composants (comme `UserService`) dépendent d'abstractions (`UserRepository`, `NotificationService`), pas directement d'implémentations concrètes (comme une base de données ou un service d'email). Cela permet de les assembler et de les faire évoluer plus facilement.

3. Spring à la Rescousse : IoC et DI

Nous avons résolu un problème : notre NotificationService ne contient plus de new. Mais cela en soulève un autre : qui est responsable de créer l'instance (EmailService ou SMSService) et de la fournir (l'injecter) à NotificationService ?

C'est là que Spring intervient avec deux concepts liés :

  1. Inversion de Contrôle (IoC) : C'est le principe général. Au lieu que votre code contrôle la création et la liaison des objets (en utilisant new), vous déléguez ce contrôle au framework. Le contrôle est "inversé". Le cœur de ce mécanisme dans Spring est l'IoC Container (ou Contexte d'Application).

  2. Injection de Dépendances (DI) : C'est le mécanisme par lequel l'IoC est mise en œuvre. Le conteneur "injecte" les dépendances requises dans les objets au moment de leur création.

Dependency Injection with IoC Container
Cette image illustre le rôle du Conteneur IoC. Basé sur la configuration (annotations Java), il crée les objets (appelés 'Beans') et injecte les dépendances nécessaires (par exemple, injecter le Bean 2 dans le Bean 1).

L'article suivant résume de façon très efficace comment Spring met en œuvre ces concepts.

Apprendre IoC et DI | Notions de Base de Spring Boot - Codefinity

Cet article de Codefinity est un excellent résumé, clair et concis, des notions de base de l'IoC et de la DI dans Spring Boot. Il présente les annotations que nous allons utiliser.

Lisez l'article depuis le début jusqu'à la section 'Quels sont les différents types de Bean ?'. Concentrez-vous sur la définition du conteneur Spring, le rôle des annotations @Component et @Service, et la manière dont @Autowired permet l'injection. C'est la base théorique de notre exercice pratique.

Les Annotations Clés

Pour dire à Spring de gérer nos objets, nous utilisons des annotations :

  • @Component : C'est l'annotation générique pour indiquer à Spring : "Cette classe est un composant, crée une instance et gère-la pour moi". Une telle instance gérée par Spring est appelée un Bean.
  • @Service : Une spécialisation de @Component. Sémantiquement, on l'utilise pour marquer les classes qui contiennent la logique métier (ex: UserService). Fonctionnellement, c'est identique à @Component, mais cela clarifie l'architecture de votre application.
  • @Autowired : Se place sur un champ, un constructeur ou une méthode set pour dire à Spring : "J'ai besoin d'une dépendance ici. Trouve le bean correspondant dans ton conteneur et injecte-le."

La vidéo suivante montre comment mettre en place ce système d'annotations de manière très pratique.

TUTO Spring : introduction au principe d'injection de dépendances avec Spring Framework

Cette vidéo de Dominique Liard montre comment implémenter l'injection de dépendances avec les annotations modernes de Spring, l'approche que nous allons utiliser.

Regardez les deux segments suivants : De 52:42 à 01:01:18 : Cette partie montre comment utiliser @ComponentScan (implicite dans @SpringBootApplication), @Component et @Autowired pour réaliser l'injection. C'est le cœur de notre leçon. De 01:01:18 à 01:03:21 : Cette courte section explique la différence sémantique entre @Component et @Service, une bonne pratique à connaître.


4. Mise en Pratique

Passons maintenant à la pratique dans le projet que vous avez créé lors de la leçon précédente. Nous allons créer un UserService qui a besoin d'un NotificationService pour fonctionner.

Ouvrez votre projet dans IntelliJ IDEA.

Étape 1 : Créer l'interface de notification

Dans votre package principal (ex: com.votreentreprise.premierprojetspring), créez un nouveau sous-package nommé services. À l'intérieur de services, créez une nouvelle interface Java nommée NotificationService.

// src/main/java/com/.../services/NotificationService.java
package com.votreentreprise.premierprojetspring.services;

public interface NotificationService {
    void sendNotification(String message);
}

Étape 2 : Créer l'implémentation par email

Dans le même package services, créez une classe EmailService qui implémente cette interface.

// src/main/java/com/.../services/EmailService.java
package com.votreentreprise.premierprojetspring.services;

import org.springframework.stereotype.Service;

@Service // Annotation clé ! On dit à Spring que c'est un bean de type Service.
public class EmailService implements NotificationService {

    @Override
    public void sendNotification(String message) {
        System.out.println("Envoi d'un EMAIL avec le message : " + message);
    }
}

L'annotation @Service est cruciale. Elle indique au conteneur Spring de créer une instance de EmailService et de la garder à disposition.

Étape 3 : Créer le service utilisateur qui a besoin de la dépendance

Toujours dans le package services, créez la classe UserService.

// src/main/java/com/.../services/UserService.java
package com.votreentreprise.premierprojetspring.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final NotificationService notificationService;

    // Injection par constructeur : la meilleure pratique !
    @Autowired
    public UserService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void registerUser(String username) {
        System.out.println("Enregistrement de l'utilisateur : " + username);
        // Utilisation du service injecté
        notificationService.sendNotification("Bienvenue, " + username + " !");
    }
}

Ici, nous utilisons l'injection par constructeur. C'est la méthode recommandée car :

  • Elle garantit que les dépendances sont disponibles dès la création de l'objet.
  • Elle permet de déclarer les champs comme final, assurant leur immuabilité.
  • Elle rend les dépendances explicites.

Note : Comme mentionné dans les ressources, si une classe n'a qu'un seul constructeur, l'annotation @Autowired sur ce dernier est optionnelle depuis Spring 4.3. Cependant, il est bon de la connaître.

Étape 4 : Tester l'injection

Comment vérifier que tout cela fonctionne sans créer d'API web pour l'instant ? Nous pouvons utiliser un CommandLineRunner. C'est un type de bean spécial dont la méthode run() est exécutée juste après le démarrage de l'application.

Ouvrez votre classe principale (...Application.java) et ajoutez-y ce bloc de code :

// Dans votre classe ...Application.java
// ...
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import com.votreentreprise.premierprojetspring.services.UserService;

@SpringBootApplication
public class PremierProjetSpringApplication {

	public static void main(String[] args) {
		SpringApplication.run(PremierProjetSpringApplication.class, args);
	}

	@Bean // On déclare un bean de type CommandLineRunner
	public CommandLineRunner commandLineRunner(UserService userService) {
		return args -> {
			System.out.println("Le CommandLineRunner démarre !");
			userService.registerUser("John Doe");
			System.out.println("Le CommandLineRunner a terminé !");
		};
	}
}

L'annotation @Bean sur la méthode indique à Spring d'exécuter cette méthode pour obtenir un bean à gérer. Notez que Spring est assez intelligent pour voir que cette méthode a besoin d'un UserService en paramètre et il va automatiquement l'injecter.

Étape 5 : Lancer l'application

Lancez votre application en cliquant sur la flèche verte à côté de votre méthode main. Observez la console. Vous devriez voir quelque chose comme :

Le CommandLineRunner démarre !
Enregistrement de l'utilisateur : John Doe
Envoi d'un EMAIL avec le message : Bienvenue, John Doe !
Le CommandLineRunner a terminé !

Ce résultat prouve que Spring a :

  1. Créé un bean EmailService.
  2. Créé un bean UserService.
  3. Injecté le bean EmailService dans le constructeur de UserService.
  4. Injecté le bean UserService dans notre CommandLineRunner.
  5. Exécuté le code qui utilise la chaîne de dépendances.

Et tout cela, sans un seul new dans notre code métier !


Conclusion

Félicitations ! Vous venez de mettre en œuvre le mécanisme le plus central de Spring. C'est un pas de géant qui vous éloigne de la programmation "manuelle" et vous fait entrer dans le monde des frameworks modernes à opinion.

Points clés à retenir :

  • Le couplage fort, causé par l'utilisation de new, rend les applications rigides et difficiles à tester.
  • La solution est le couplage faible en dépendant d'interfaces (abstractions) plutôt que de classes concrètes.
  • L'Inversion de Contrôle (IoC) est le principe où le framework (Spring) prend en charge la création et la gestion des objets (beans).
  • L'Injection de Dépendances (DI) est le mécanisme par lequel Spring fournit à un bean les autres beans dont il a besoin.
  • @Component et ses spécialisations (@Service, @Repository, @Controller) marquent une classe comme un bean à gérer par Spring.
  • @Autowired demande à Spring d'injecter une dépendance, de préférence via le constructeur.

Dans la prochaine leçon, nous mettrons ces services à profit. Nous allons créer des contrôleurs REST avec @RestController et @RequestMapping pour exposer la logique de notre UserService via des points de terminaison HTTP, la rendant ainsi accessible depuis un frontend ou tout autre client web.