TP5.1 - Inyección de Dependencias Avanzada
Objetivos
Al final de este TP, usted será capaz de:
- ✅ Utilizar los 3 tipos de inyección (constructor, setter, campo)
- ✅ Elegir entre varias implementaciones con @Qualifier
- ✅ Gestionar dependencias opcionales
- ✅ 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:
-
Vaya a https://start.spring.io/
-
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
-
Dependencias a agregar:
- Spring Web
-
Haga clic en GENERATE
-
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 ausenteifPresent()→ 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
| Tipo | Sintaxis | ¿Obligatorio? | ¿Inmutable? | Uso |
|---|---|---|---|---|
| Constructor | public Service(Dep d) | ✅ Sí | ✅ Sí (final) | Recomendado para dependencias obligatorias |
| Setter | @Autowired(required=false) | ❌ No | ❌ No | Dependencias 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:
- ¿Por qué se prefiere la inyección por constructor?
- ¿Cuál es la diferencia entre @Primary y @Qualifier?
- ¿Cómo gestionar una dependencia realmente opcional?
- ¿Cómo inyectar todas las implementaciones de una interfaz?
- ¿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:
- Interfaz
NotificationChannel - Implementaciones:
EmailChannel,SmsChannel,PushChannel - Servicio
NotificationOrchestratorque:- 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.