Saltar al contenido principal

TP5.1 - Inyección de Dependencias Avanzada

Objetivos​

Al final de este TP, usted será capaz de:

  1. ✅ Utilizar los 3 tipos de inyección (constructor, setter, campo)
  2. ✅ Elegir entre varias implementaciones con @Qualifier
  3. ✅ Gestionar dependencias opcionales
  4. ✅ Comprender y resolver dependencias circulares

Duración estimada: 1h30


Prerrequisitos​

  • ✅ Capítulos 1 y 2 del Módulo 5 completados
  • ✅ Maven 3.9+ instalado
  • ✅ Java 17+ instalado
  • ✅ VS Code con extensiones de Java

Preparación: Crear el Proyecto​

Paso 1: Generar el Proyecto con Spring Initializr​

Vía el sitio web:

  1. Vaya a https://start.spring.io/

  2. Configure:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0 (o última versión)
    • Group: com.example
    • Artifact: injection-demo
    • Name: injection-demo
    • Package name: com.example.injection
    • Packaging: Jar
    • Java: 17
  3. Dependencias a agregar:

    • Spring Web
  4. Haga clic en GENERATE

  5. Descomprima y abra en VS Code


Paso 2: Estructura del Proyecto​

Cree la siguiente estructura:

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

Ejercicio 1: Inyección por Constructor ✅​

Objetivo​

Crear un servicio con inyección por constructor (buena práctica).

Paso 1.1: Crear la Interfaz EmailService​

Archivo: service/EmailService.java

package com.example.injection.service;

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

Qué hacemos: Crear una interfaz para tener varias implementaciones posibles.


Paso 1.2: Crear la Implementación GmailService​

Archivo: 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";
}
}

Puntos clave:

  • @Service → Spring gestiona este Bean
  • Implementa EmailService

Paso 1.3: Crear NotificationService con Inyección por Constructor​

Archivo: service/NotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class NotificationService {

// Dependencia declarada como 'final' (inmutable)
private final EmailService emailService;

// Inyección por constructor (BUENA PRÁCTICA ✅)
public NotificationService(EmailService emailService) {
this.emailService = emailService;
System.out.println("NotificationService creado con: " +
emailService.getProviderName());
}

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

Puntos clave:

  • private final → Inmutable
  • Inyección vía constructor
  • No es necesario @Autowired (opcional en constructor único)

Paso 1.4: Crear el Controller de Prueba​

Archivo: 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;

// Inyección por constructor también aquí
public TestController(NotificationService notificationService) {
this.notificationService = notificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test inyección por constructor");
return "✅ Inyección por constructor probada - Ver consola";
}
}

Paso 1.5: Probar​

Iniciar la aplicación:

mvn clean install -DskipTests spring-boot:run

Probar en el navegador:

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

La consola debería mostrar:

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

✅ Resultado esperado: "✅ Inyección por constructor probada - Ver consola"


Ejercicio 2: Gestionar Varias Implementaciones con @Qualifier​

Objetivo​

Tener 2 implementaciones de EmailService y elegir cuál utilizar.

Paso 2.1: Crear SendGridService​

Archivo: service/SendGridService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service("sendGridService") // ← Nombre explícito
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";
}
}

Paso 2.2: Problema - ¡Dos Implementaciones!​

Reinicie la aplicación:

mvn clean install -DskipTests spring-boot:run

Debería ver un ERROR:

***************************
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

¡Esto es normal! Spring no sabe cuál elegir.


Paso 2.3: Solución 1 - @Primary​

Modifique GmailService.java:

package com.example.injection.service;

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

@Service
@Primary // ← Implementación por defecto
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";
}
}

Reinicie: ¡La aplicación inicia ahora! Gmail se utiliza por defecto.

Pruebe:

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

Consola:

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

Paso 2.4: Solución 2 - @Qualifier para Elegir​

Cree un nuevo servicio que utilice SendGrid:

Archivo: 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;

// Usar @Qualifier para elegir SendGrid
public SendGridNotificationService(
@Qualifier("sendGridService") EmailService emailService
) {
this.emailService = emailService;
System.out.println("SendGridNotificationService creado con: " +
emailService.getProviderName());
}

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

Paso 2.5: Probar @Qualifier​

Agregue un método en 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 con Gmail");
return "✅ Gmail utilizado - Ver consola";
}

@GetMapping("/qualifier")
public String testQualifier() {
sendGridNotificationService.notifyUser("user@example.com",
"Test con SendGrid");
return "✅ SendGrid utilizado vía @Qualifier - Ver consola";
}
}

Pruebe:

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

Consola:

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

✅ ¡Tiene 2 servicios usando implementaciones diferentes!


Ejercicio 3: Dependencias Opcionales​

Objetivo​

Crear un servicio que pueda funcionar con o sin una dependencia.

Paso 3.1: Crear un SmsService (Opcional)​

Archivo: service/SmsService.java

package com.example.injection.service;

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

¡No cree una implementación por ahora!


Paso 3.2: Crear CompleteNotificationService con Dependencia Opcional​

Archivo: 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; // ← ¡Opcional!

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

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

public void notify(String email, String phone, String message) {
// Email siempre enviado
emailService.send(email, message);

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

Puntos clave:

  • Optional<SmsService> → Puede estar ausente
  • ifPresent() → Ejecutado solo si está presente

Paso 3.3: Probar Sin SmsService​

Agregue en 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 dependencia opcional");
return "✅ Email enviado, SMS no disponible - Ver consola";
}

Pruebe:

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

Consola:

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

✅ ¡La aplicación funciona incluso sin SmsService!


Paso 3.4: Agregar SmsService y Volver a Probar​

Archivo: 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);
}
}

Reinicie y pruebe:

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

Consola:

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

✅ ¡Ahora ambos son utilizados!


Ejercicio 4: Inyectar Todas las Implementaciones​

Objetivo​

Recuperar TODAS las implementaciones de una interfaz.

Paso 4.1: Crear MultiChannelNotificationService​

Archivo: 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 inyecta una Lista con TODAS las implementaciones
public MultiChannelNotificationService(List<EmailService> allEmailServices) {
this.allEmailServices = allEmailServices;
System.out.println("MultiChannelNotificationService creado con " +
allEmailServices.size() + " servicios de email:");
allEmailServices.forEach(service ->
System.out.println(" - " + service.getProviderName())
);
}

public void broadcastEmail(String to, String message) {
System.out.println("=== Envío vía TODOS los providers ===");
allEmailServices.forEach(service -> service.send(to, message));
}
}

Paso 4.2: Probar​

Agregue en 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",
"Mensaje broadcast");
return "✅ Envío vía todos los providers - Ver consola";
}

Pruebe:

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

Consola:

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

✅ ¡Se utilizan ambas implementaciones!


Ejercicio 5: Inyección por Setter (Dependencias Opcionales)​

Objetivo​

Utilizar la inyección por setter para dependencias realmente opcionales.

Paso 5.1: Crear LoggerService con Setter​

Archivo: 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; // ← No 'final', puede ser null

// Inyección por setter - OPCIONAL
@Autowired(required = false) // ← required = false
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
System.out.println("SMS Service inyectado en 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 no disponible");
}
}
}

Puntos clave:

  • @Autowired(required = false) → No obligatorio
  • Verificación if (smsService != null)

Paso 5.2: Probar​

Agregue en TestController.java:

private final LoggerService loggerService;

@GetMapping("/setter")
public String testSetter() {
loggerService.logNotification("Test inyección por setter");
return "✅ Inyección por setter probada - Ver consola";
}

Pruebe:

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

Consola (con SmsService):

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

Resumen: Comparación de los Métodos​

Tabla Comparativa​

TipoSintaxis¿Obligatorio?¿Inmutable?Uso
Constructorpublic Service(Dep d)✅ Sí✅ Sí (final)Recomendado para dependencias obligatorias
Setter@Autowired(required=false)❌ No❌ NoDependencias opcionales
Campo@Autowired private Dep d⚠️ Sí❌ No❌ A evitar
Optional<T>Optional<Dep> d❌ No✅ SíDependencias opcionales modernas
List<T>List<Dep> all✅ Sí✅ SíTodas las implementaciones

Código Final Completo​

Su proyecto ahora debería tener:

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

Autoevaluación​

Responda las siguientes preguntas:

  1. ¿Por qué se prefiere la inyección por constructor?
  2. ¿Cuál es la diferencia entre @Primary y @Qualifier?
  3. ¿Cómo gestionar una dependencia realmente opcional?
  4. ¿Cómo inyectar todas las implementaciones de una interfaz?
  5. ¿Cuándo usar Optional<T> vs @Autowired(required=false)?

Si puede responder a todas estas preguntas, ¡ha dominado la inyección de dependencias! ✅


Challenge Bonus 🎯​

Cree un sistema de notificación multicanal:

  1. Interfaz NotificationChannel
  2. Implementaciones: EmailChannel, SmsChannel, PushChannel
  3. Servicio NotificationOrchestrator que:
    • Inyecta todos los canales
    • Permite activar/desactivar canales
    • Envía por todos los canales activos

Tiempo estimado: 30 minutos


Próximo Paso​

¡Ahora domina la inyección de dependencias! Pase al TP2 para aprender sobre Beans y Ámbitos.

➡️ TP5.2: Beans y Ámbitos

⬅️ Volver al Módulo 5