Aller au contenu principal

Chapitre 2: Injection de Dépendances (DI)

Introduction​

Dans le Chapitre 1, vous avez compris POURQUOI l'IoC existe. Maintenant, vous allez apprendre COMMENT Spring injecte les dépendances.

Ce que vous allez apprendre​

  1. Les 3 types d'injection de dépendances
  2. Pourquoi l'injection par constructeur est préférée
  3. Comment utiliser @Autowired, @Inject, @Resource
  4. Gérer les dépendances optionnelles

Durée: 45 minutes


Les 3 Types d'Injection​

Spring propose 3 façons d'injecter des dépendances:

  1. Injection par Constructeur ✅ (Recommandée)
  2. Injection par Setter
  3. Injection par Champ ⚠️ (Déconseillée)

Voyons chacune en détail.


1. Injection par Constructeur ✅​

Syntaxe​

@Service
public class OrderService {

// Dépendances déclarées comme 'final'
private final EmailService emailService;
private final PaymentService paymentService;

// Injection via le constructeur
public OrderService(EmailService emailService,
PaymentService paymentService) {
this.emailService = emailService;
this.paymentService = paymentService;
}
}

Comment Ça Marche?​

Au démarrage de l'application:

1. Spring trouve OrderService
↓
2. Spring voit le constructeur avec 2 paramètres
↓
3. Spring crée EmailService
↓
4. Spring crée PaymentService
↓
5. Spring crée OrderService en passant les 2 dépendances
↓
6. OrderService est prêt!

Avantages de l'Injection par Constructeur​

Avantage 1: Immutabilité​

private final EmailService emailService;
// ^^^^^
// Impossible de modifier après la création!

Pourquoi c'est bien?

  • ✅ Thread-safe (pas de problèmes de concurrence)
  • ✅ Prévisible (ne peut pas changer)
  • ✅ Moins de bugs

Avantage 2: Dépendances Obligatoires​

public OrderService(EmailService emailService) {
this.emailService = emailService;
}

Si Spring ne trouve pas EmailService:

❌ Application refuse de démarrer!
❌ Erreur claire au démarrage

C'est BIEN! Vous découvrez le problème immédiatement, pas en production!


Avantage 3: Facile à Tester​

@Test
public void testOrderService() {
// Créer des fakes
EmailService fakeEmail = new FakeEmailService();
PaymentService fakePayment = new FakePaymentService();

// Injecter facilement via le constructeur
OrderService service = new OrderService(fakeEmail, fakePayment);

// Tester
service.processOrder(new Order());
}

Pas besoin de Spring pour tester!


Avantage 4: Pas Besoin de @Autowired​

Depuis Spring 4.3, @Autowired est optionnel sur les constructeurs:

@Service
public class OrderService {
private final EmailService emailService;

// Pas besoin de @Autowired!
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Spring détecte automatiquement le constructeur!


Résumé: Injection par Constructeur​

✅ Dépendances immutables (final)
✅ Dépendances obligatoires
✅ Facile à tester
✅ Pas besoin de @Autowired
✅ Recommandée par Spring

👉 UTILISEZ TOUJOURS L'INJECTION PAR CONSTRUCTEUR!

2. Injection par Setter​

Syntaxe​

@Service
public class OrderService {

// Dépendance NON final
private EmailService emailService;

// Injection via un setter
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}

Comment Ça Marche?​

1. Spring crée OrderService (constructeur vide)
↓
2. Spring crée EmailService
↓
3. Spring appelle setEmailService(emailService)
↓
4. OrderService est prêt

Quand Utiliser?​

Cas d'usage: Dépendances optionnelles

@Service
public class OrderService {

private EmailService emailService;
private SmsService smsService; // Optionnel

@Autowired(required = false) // ← Optionnel!
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}

public void notifyCustomer(String message) {
// Email toujours envoyé
emailService.send(message);

// SMS seulement si disponible
if (smsService != null) {
smsService.send(message);
}
}
}

Inconvénients​

❌ Dépendances mutables (pas 'final')
❌ Peut être null (risque de NullPointerException)
❌ Objet créé dans un état incomplet
❌ Plus difficile à tester

Utilisez seulement pour des dépendances optionnelles!


3. Injection par Champ ⚠️​

Syntaxe​

@Service
public class OrderService {

// Injection directe dans le champ
@Autowired
private EmailService emailService;

@Autowired
private PaymentService paymentService;
}

Comment Ça Marche?​

1. Spring crée OrderService
↓
2. Spring utilise la RÉFLEXION pour accéder aux champs privés
↓
3. Spring injecte les dépendances directement
↓
4. OrderService est prêt

Pourquoi C'est Déconseillé?​

Problème 1: Impossible de Rendre 'final'​

@Autowired
private EmailService emailService; // ❌ Pas 'final' possible

Problème 2: Difficile à Tester​

@Test
public void testOrderService() {
OrderService service = new OrderService();

// ❌ Comment injecter les dépendances?
// Les champs sont privés!

// Il faut utiliser la réflexion... compliqué!
}

Problème 3: Dépendances Cachées​

@Service
public class OrderService {
@Autowired
private Dep1 dep1;
@Autowired
private Dep2 dep2;
@Autowired
private Dep3 dep3;
@Autowired
private Dep4 dep4;
@Autowired
private Dep5 dep5;
// ... 10 dépendances!
}

Problème: On ne voit pas facilement toutes les dépendances!

Avec constructeur:

public OrderService(Dep1 d1, Dep2 d2, Dep3 d3, Dep4 d4, Dep5 d5) {
// ❌ WOW! Trop de dépendances!
// ➡️ Signal que la classe fait trop de choses
}

Résumé: Injection par Champ​

❌ Pas immutable
❌ Difficile à tester
❌ Cache les problèmes de design
❌ Utilise la réflexion (plus lent)

👉 ÉVITEZ L'INJECTION PAR CHAMP!

Comparaison des 3 Types​

CritèreConstructeurSetterChamp
Immutabilité (final)✅ Oui❌ Non❌ Non
Dépendances obligatoires✅ Oui❌ Non⚠️ Pas clair
Facile à tester✅ Oui⚠️ Moyen❌ Non
Lisibilité✅ Claire✅ Claire⚠️ Cachée
@Autowired nécessaire❌ Non✅ Oui✅ Oui
Recommandation✅ UTILISEZ⚠️ Optionnel❌ ÉVITEZ

Annotations d'Injection​

Spring supporte 3 annotations pour l'injection:

1. @Autowired (Spring)​

@Service
public class OrderService {

private final EmailService emailService;

@Autowired // Annotation Spring
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Origine: Spring Framework
Utilisation: La plus courante


2. @Inject (Java Standard)​

import jakarta.inject.Inject;

@Service
public class OrderService {

private final EmailService emailService;

@Inject // Annotation Jakarta (Java standard)
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Origine: Jakarta EE (anciennement Java EE)
Utilisation: Si vous voulez du code portable (pas lié à Spring)


3. @Resource (Java Standard)​

import jakarta.annotation.Resource;

@Service
public class OrderService {

@Resource(name = "emailService") // Injection par nom
private EmailService emailService;
}

Origine: Jakarta EE
Différence: Injecte par nom au lieu du type


Laquelle Choisir?​

Recommandation: @Autowired (ou aucune pour constructeur)

Pourquoi?
✅ Standard Spring
✅ Optionnel sur constructeur
✅ Plus d'options (required, @Qualifier)
✅ Mieux documentée

Gestion de Plusieurs Implémentations​

Problème​

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

@Service
public class GmailService implements EmailService { ... }

@Service
public class SendGridService implements EmailService { ... }

// ❌ Spring ne sait pas laquelle choisir!

Solution 1: @Primary​

@Service
@Primary // ← Cette implémentation par défaut
public class GmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("Envoi via Gmail à " + to);
}
}

@Service
public class SendGridService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("Envoi via SendGrid à " + to);
}
}

Spring utilisera GmailService par défaut!


Solution 2: @Qualifier​

@Service
public class OrderService {

private final EmailService emailService;

public OrderService(@Qualifier("sendGridService") EmailService emailService) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// Nom spécifique du bean
this.emailService = emailService;
}
}

Spring utilisera spécifiquement SendGridService!


Solution 3: Nommage Explicite​

@Service("gmailEmailService")  // ← Nom explicite
public class GmailService implements EmailService { ... }

@Service("sendGridEmailService") // ← Nom explicite
public class SendGridService implements EmailService { ... }

// Utilisation
public OrderService(@Qualifier("sendGridEmailService") EmailService service) {
this.emailService = service;
}

Dépendances Optionnelles​

Avec @Autowired(required = false)​

@Service
public class NotificationService {

private final EmailService emailService;
private SmsService smsService; // Optionnel

// Obligatoire
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}

// Optionnel
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}

public void notify(String message) {
emailService.send(message); // Toujours

if (smsService != null) {
smsService.send(message); // Si disponible
}
}
}

Avec Optional<T>​

import java.util.Optional;

@Service
public class NotificationService {

private final EmailService emailService;
private final Optional<SmsService> smsService;

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

public void notify(String message) {
emailService.send(message);

smsService.ifPresent(sms -> sms.send(message));
// ^^^^^^^^^
// Appelé seulement si présent
}
}

Exemple Complet​

// Interface
public interface PaymentProcessor {
void process(Order order);
}

// Implémentation 1
@Service
@Primary // ← Par défaut
public class StripePaymentProcessor implements PaymentProcessor {

@Override
public void process(Order order) {
System.out.println("Paiement via Stripe: " + order.getTotal());
}
}

// Implémentation 2
@Service("paypalProcessor")
public class PayPalPaymentProcessor implements PaymentProcessor {

@Override
public void process(Order order) {
System.out.println("Paiement via PayPal: " + order.getTotal());
}
}

// Utilisation - Option 1: Utiliser @Primary
@Service
public class OrderService {

private final PaymentProcessor paymentProcessor;

// Spring injecte StripePaymentProcessor (@Primary)
public OrderService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
}

// Utilisation - Option 2: Spécifier avec @Qualifier
@Service
public class SpecialOrderService {

private final PaymentProcessor paymentProcessor;

// Force l'utilisation de PayPal
public SpecialOrderService(
@Qualifier("paypalProcessor") PaymentProcessor paymentProcessor
) {
this.paymentProcessor = paymentProcessor;
}
}

Résumé du Chapitre​

Les 3 Types d'Injection​

// 1. Constructeur ✅ RECOMMANDÉ
public MyService(Dependency dep) {
this.dep = dep;
}

// 2. Setter ⚠️ Pour dépendances optionnelles
@Autowired(required = false)
public void setDependency(Dependency dep) {
this.dep = dep;
}

// 3. Champ ❌ À ÉVITER
@Autowired
private Dependency dep;

Choix d'Implémentation​

// Option 1: @Primary
@Primary
@Service
public class DefaultImpl implements MyInterface { }

// Option 2: @Qualifier
public MyService(@Qualifier("specificImpl") MyInterface service) { }

Points Clés​

✅ Toujours utiliser l'injection par constructeur
✅ Déclarer les dépendances comme final
✅ @Autowired optionnel sur constructeur
✅ @Primary pour implémentation par défaut
✅ @Qualifier pour choix spécifique
⚠️ Setter seulement pour dépendances optionnelles
❌ Éviter l'injection par champ


Auto-Évaluation​

  1. Quels sont les 3 types d'injection?
  2. Pourquoi l'injection par constructeur est-elle préférée?
  3. Quelle est la différence entre @Primary et @Qualifier?
  4. Comment rendre une dépendance optionnelle?

Prochaine Étape​

Vous savez maintenant COMMENT injecter des dépendances. Passons à la création et la gestion des Beans!

➡️ Chapitre 3: Beans et Contexte Spring

⬅️ Chapitre précédent: IoC

⬅️ Retour au Module 5