Aller au contenu principal

TP5.1 - Injection de Dépendances Avancée

Objectifs​

À la fin de ce TP, vous serez capable de:

  1. ✅ Utiliser les 3 types d'injection (constructeur, setter, champ)
  2. ✅ Choisir entre plusieurs implémentations avec @Qualifier
  3. ✅ Gérer des dépendances optionnelles
  4. ✅ Comprendre et résoudre les dépendances circulaires

Durée estimée: 1h30


Prérequis​

  • ✅ Chapitres 1 et 2 du Module 5 complétés
  • ✅ Maven 3.9+ installé
  • ✅ Java 17+ installé
  • ✅ VS Code avec extensions Java

Préparation: Créer le Projet​

Étape 1: Générer le Projet avec Spring Initializr​

Via le site web:

  1. Allez sur https://start.spring.io/

  2. Configurez:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0 (ou dernière version)
    • Group: com.example
    • Artifact: injection-demo
    • Name: injection-demo
    • Package name: com.example.injection
    • Packaging: Jar
    • Java: 17
  3. Dépendances à ajouter:

    • Spring Web
  4. Cliquez sur GENERATE

  5. Dézippez et ouvrez dans VS Code


Étape 2: Structure du Projet​

Créez la structure suivante:

src/main/java/com/example/injection/
├── InjectionDemoApplication.java
├── service/
│ ├── EmailService.java
│ ├── GmailService.java
│ ├── SendGridService.java
│ └── NotificationService.java
├── controller/
│ └── TestController.java
└── config/
└── AppConfig.java

Exercice 1: Injection par Constructeur ✅​

Objectif​

Créer un service avec injection par constructeur (bonne pratique).

Étape 1.1: Créer l'Interface EmailService​

Fichier: service/EmailService.java

package com.example.injection.service;

public interface EmailService {
void send(String to, String message);
String getProviderName();
}

Ce qu'on fait: Créer une interface pour avoir plusieurs implémentations possibles.


Étape 1.2: Créer l'Implémentation GmailService​

Fichier: service/GmailService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] Envoi email à " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

Points clés:

  • @Service → Spring gère ce Bean
  • Implémente EmailService

Étape 1.3: Créer le NotificationService avec Injection par Constructeur​

Fichier: service/NotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class NotificationService {

// Dépendance déclarée comme 'final' (immutable)
private final EmailService emailService;

// Injection par constructeur (BONNE PRATIQUE ✅)
public NotificationService(EmailService emailService) {
this.emailService = emailService;
System.out.println("NotificationService créé avec: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== NotificationService ===");
emailService.send(email, message);
}
}

Points clés:

  • private final → Immutable
  • Injection via constructeur
  • Pas besoin de @Autowired (optionnel sur constructeur unique)

Étape 1.4: Créer le Controller de Test​

Fichier: controller/TestController.java

package com.example.injection.controller;

import com.example.injection.service.NotificationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;

// Injection par constructeur aussi ici
public TestController(NotificationService notificationService) {
this.notificationService = notificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test injection par constructeur");
return "✅ Injection par constructeur testée - Voir console";
}
}

Étape 1.5: Tester​

Démarrer l'application:

mvn clean install -DskipTests spring-boot:run

Tester dans le navigateur:

http://localhost:8080/api/test/constructor

Console devrait afficher:

NotificationService créé avec: Gmail
=== NotificationService ===
[Gmail] Envoi email à user@example.com: Test injection par constructeur

✅ Résultat attendu: "✅ Injection par constructeur testée - Voir console"


Exercice 2: Gérer Plusieurs Implémentations avec @Qualifier​

Objectif​

Avoir 2 implémentations d'EmailService et choisir laquelle utiliser.

Étape 2.1: Créer SendGridService​

Fichier: service/SendGridService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service("sendGridService") // ← Nom explicite
public class SendGridService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[SendGrid] Envoi email à " + to + ": " + message);
}

@Override
public String getProviderName() {
return "SendGrid";
}
}

Étape 2.2: Problème - Deux Implémentations!​

Redémarrez l'application:

mvn clean install -DskipTests spring-boot:run

Vous devriez voir une ERREUR:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.injection.service.NotificationService
required a single bean, but 2 were found:
- gmailService: defined in file [...]
- sendGridService: defined in file [...]

Action:

Consider marking one of the beans as @Primary, or use @Qualifier

C'est normal! Spring ne sait pas laquelle choisir.


Étape 2.3: Solution 1 - @Primary​

Modifiez GmailService.java:

package com.example.injection.service;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Service
@Primary // ← Implémentation par défaut
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] Envoi email à " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

Redémarrez: L'application démarre maintenant! Gmail est utilisé par défaut.

Testez:

http://localhost:8080/api/test/constructor

Console:

[Gmail] Envoi email à user@example.com: ...

Étape 2.4: Solution 2 - @Qualifier pour Choisir​

Créez un nouveau service qui utilise SendGrid:

Fichier: service/SendGridNotificationService.java

package com.example.injection.service;

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

@Service
public class SendGridNotificationService {

private final EmailService emailService;

// Utiliser @Qualifier pour choisir SendGrid
public SendGridNotificationService(
@Qualifier("sendGridService") EmailService emailService
) {
this.emailService = emailService;
System.out.println("SendGridNotificationService créé avec: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== SendGridNotificationService ===");
emailService.send(email, message);
}
}

Étape 2.5: Tester @Qualifier​

Ajoutez une méthode dans TestController.java:

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;
private final SendGridNotificationService sendGridNotificationService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test avec Gmail");
return "✅ Gmail utilisé - Voir console";
}

@GetMapping("/qualifier")
public String testQualifier() {
sendGridNotificationService.notifyUser("user@example.com",
"Test avec SendGrid");
return "✅ SendGrid utilisé via @Qualifier - Voir console";
}
}

Testez:

http://localhost:8080/api/test/qualifier

Console:

=== SendGridNotificationService ===
[SendGrid] Envoi email à user@example.com: Test avec SendGrid

✅ Vous avez 2 services utilisant des implémentations différentes!


Exercice 3: Dépendances Optionnelles​

Objectif​

Créer un service qui peut fonctionner avec ou sans une dépendance.

Étape 3.1: Créer un SmsService (Optionnel)​

Fichier: service/SmsService.java

package com.example.injection.service;

public interface SmsService {
void send(String phone, String message);
}

Ne créez PAS d'implémentation pour l'instant!


Étape 3.2: Créer CompleteNotificationService avec Dépendance Optionnelle​

Fichier: service/CompleteNotificationService.java

package com.example.injection.service;

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

import java.util.Optional;

@Service
public class CompleteNotificationService {

private final EmailService emailService;
private final Optional<SmsService> smsService; // ← Optionnel!

public CompleteNotificationService(
EmailService emailService,
Optional<SmsService> smsService // ← Wrapper Optional
) {
this.emailService = emailService;
this.smsService = smsService;

System.out.println("CompleteNotificationService créé");
System.out.println("- Email: " + emailService.getProviderName());
System.out.println("- SMS: " + (smsService.isPresent() ? "Disponible" : "Non disponible"));
}

public void notify(String email, String phone, String message) {
// Email toujours envoyé
emailService.send(email, message);

// SMS seulement si disponible
smsService.ifPresent(sms -> sms.send(phone, message));
}
}

Points clés:

  • Optional<SmsService> → Peut être absent
  • ifPresent() → Exécuté seulement si présent

Étape 3.3: Tester Sans SmsService​

Ajoutez dans TestController.java:

private final CompleteNotificationService completeService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
}

@GetMapping("/optional")
public String testOptional() {
completeService.notify("user@example.com", "123-456-7890",
"Test dépendance optionnelle");
return "✅ Email envoyé, SMS non disponible - Voir console";
}

Testez:

http://localhost:8080/api/test/optional

Console:

CompleteNotificationService créé
- Email: Gmail
- SMS: Non disponible
[Gmail] Envoi email à user@example.com: Test dépendance optionnelle

✅ L'application fonctionne même sans SmsService!


Étape 3.4: Ajouter SmsService et Retester​

Fichier: service/SimpleSmsService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class SimpleSmsService implements SmsService {

@Override
public void send(String phone, String message) {
System.out.println("[SMS] Envoi à " + phone + ": " + message);
}
}

Redémarrez et testez:

http://localhost:8080/api/test/optional

Console:

CompleteNotificationService créé
- Email: Gmail
- SMS: Disponible
[Gmail] Envoi email à user@example.com: Test dépendance optionnelle
[SMS] Envoi à 123-456-7890: Test dépendance optionnelle

✅ Maintenant les 2 sont utilisés!


Exercice 4: Injecter Toutes les Implémentations​

Objectif​

Récupérer TOUTES les implémentations d'une interface.

Étape 4.1: Créer MultiChannelNotificationService​

Fichier: service/MultiChannelNotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MultiChannelNotificationService {

private final List<EmailService> allEmailServices;

// Spring injecte une Liste avec TOUTES les implémentations
public MultiChannelNotificationService(List<EmailService> allEmailServices) {
this.allEmailServices = allEmailServices;
System.out.println("MultiChannelNotificationService créé avec " +
allEmailServices.size() + " services email:");
allEmailServices.forEach(service ->
System.out.println(" - " + service.getProviderName())
);
}

public void broadcastEmail(String to, String message) {
System.out.println("=== Envoi via TOUS les providers ===");
allEmailServices.forEach(service -> service.send(to, message));
}
}

Étape 4.2: Tester​

Ajoutez dans TestController.java:

private final MultiChannelNotificationService multiChannelService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService,
MultiChannelNotificationService multiChannelService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
this.multiChannelService = multiChannelService;
}

@GetMapping("/all")
public String testAllImplem() {
multiChannelService.broadcastEmail("user@example.com",
"Message broadcast");
return "✅ Envoi via tous les providers - Voir console";
}

Testez:

http://localhost:8080/api/test/all

Console:

=== Envoi via TOUS les providers ===
[Gmail] Envoi email à user@example.com: Message broadcast
[SendGrid] Envoi email à user@example.com: Message broadcast

✅ Les 2 implémentations sont utilisées!


Exercice 5: Injection par Setter (Dépendances Optionnelles)​

Objectif​

Utiliser l'injection par setter pour des dépendances vraiment optionnelles.

Étape 5.1: Créer LoggerService avec Setter​

Fichier: service/LoggerService.java

package com.example.injection.service;

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

@Service
public class LoggerService {

private SmsService smsService; // ← Pas 'final', peut être null

// Injection par setter - OPTIONNELLE
@Autowired(required = false) // ← required = false
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
System.out.println("SMS Service injecté dans LoggerService");
}

public void logNotification(String message) {
System.out.println("[LOG] " + message);

if (smsService != null) {
System.out.println("[LOG] SMS Service disponible");
} else {
System.out.println("[LOG] SMS Service non disponible");
}
}
}

Points clés:

  • @Autowired(required = false) → Pas obligatoire
  • Vérification if (smsService != null)

Étape 5.2: Tester​

Ajoutez dans TestController.java:

private final LoggerService loggerService;

@GetMapping("/setter")
public String testSetter() {
loggerService.logNotification("Test injection par setter");
return "✅ Injection par setter testée - Voir console";
}

Testez:

http://localhost:8080/api/test/setter

Console (avec SmsService):

SMS Service injecté dans LoggerService
[LOG] Test injection par setter
[LOG] SMS Service disponible

Récapitulatif: Comparaison des Méthodes​

Tableau Comparatif​

TypeSyntaxeObligatoire?Immutable?Usage
Constructeurpublic Service(Dep d)✅ Oui✅ Oui (final)Recommandé pour dépendances obligatoires
Setter@Autowired(required=false)❌ Non❌ NonDépendances optionnelles
Champ@Autowired private Dep d⚠️ Oui❌ Non❌ À éviter
Optional<T>Optional<Dep> d❌ Non✅ OuiDépendances optionnelles modernes
List<T>List<Dep> all✅ Oui✅ OuiToutes les implémentations

Code Final Complet​

Votre projet devrait maintenant avoir:

✅ GmailService (@Primary)
✅ SendGridService
✅ NotificationService (constructeur + Gmail)
✅ SendGridNotificationService (constructeur + @Qualifier)
✅ CompleteNotificationService (Optional<SmsService>)
✅ SimpleSmsService (optionnel)
✅ MultiChannelNotificationService (List<EmailService>)
✅ LoggerService (setter + required=false)

Auto-Évaluation​

Répondez aux questions suivantes:

  1. Pourquoi l'injection par constructeur est-elle préférée?
  2. Quelle est la différence entre @Primary et @Qualifier?
  3. Comment gérer une dépendance vraiment optionnelle?
  4. Comment injecter toutes les implémentations d'une interface?
  5. Quand utiliser Optional<T> vs @Autowired(required=false)?

Si vous pouvez répondre à toutes ces questions, vous avez maîtrisé l'injection de dépendances! ✅


Challenge Bonus 🎯​

Créez un système de notification multi-canaux:

  1. Interface NotificationChannel
  2. Implémentations: EmailChannel, SmsChannel, PushChannel
  3. Service NotificationOrchestrator qui:
    • Injecte tous les canaux
    • Permet d'activer/désactiver des canaux
    • Envoie sur tous les canaux actifs

Temps estimé: 30 minutes


Prochaine Étape​

Vous maîtrisez maintenant l'injection de dépendances! Passez au TP2 pour apprendre les Beans et les Scopes.

➡️ TP5.2: Beans et Scopes

⬅️ Retour au Module 5