TP5.2 - Beans y Ámbitos
Objetivos
Al final de este TP, usted será capaz de:
- ✅ Crear Beans con @Bean y @Configuration
- ✅ Comprender la diferencia entre Singleton y Prototype
- ✅ Utilizar @PostConstruct y @PreDestroy
- ✅ Crear Beans condicionales
Duración estimada: 1h30
Prerrequisitos
- ✅ Capítulo 3 del Módulo 5 completado
- ✅ TP5.1 completado
- ✅ Maven 3.9+ y Java 17+
Preparación: Nuevo Proyecto
Crear el Proyecto
Vía Spring Initializr (https://start.spring.io/):
- Project: Maven
- Spring Boot: 3.2.0
- Group:
com.example - Artifact:
beans-demo - Dependencies: Spring Web
Estructura:
src/main/java/com/example/beans/
├── BeansDemoApplication.java
├── service/
│ ├── CounterService.java
│ ├── ReportGenerator.java
│ └── CacheService.java
├── config/
│ └── AppConfig.java
└── controller/
└── TestController.java
Ejercicio 1: Singleton vs Prototype
Objetivo
Comprender la diferencia entre un Bean Singleton (1 instancia) y Prototype (varias instancias).
Paso 1.1: Crear CounterService (Singleton por defecto)
Archivo: service/CounterService.java
package com.example.beans.service;
import org.springframework.stereotype.Service;
@Service // Singleton por defecto
public class CounterService {
private int counter = 0;
public void increment() {
counter++;
System.out.println("Counter incrementé: " + counter);
}
public int getCounter() {
return counter;
}
}
Puntos clave:
@Service→ Singleton por defectocountercompartido entre todos los usos
Paso 1.2: Probar el Singleton
Archivo: controller/TestController.java
package com.example.beans.controller;
import com.example.beans.service.CounterService;
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 CounterService counterService;
public TestController(CounterService counterService) {
this.counterService = counterService;
}
@GetMapping("/singleton")
public String testSingleton() {
counterService.increment();
return "Counter: " + counterService.getCounter() +
" (Singleton - misma instancia)";
}
}
Paso 1.3: Probar
Inicie:
mvn clean install -DskipTests spring-boot:run
Pruebe varias veces:
http://localhost:8080/api/test/singleton
Resultado:
1ª petición: Counter: 1
2ª petición: Counter: 2
3ª petición: Counter: 3
...
✅ Es la MISMA instancia! El contador sigue aumentando.
Paso 1.4: Crear ReportGenerator (Prototype)
Archivo: service/ReportGenerator.java
package com.example.beans.service;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@Scope("prototype") // ← ¡Nueva instancia cada vez!
public class ReportGenerator {
private String reportId;
private List<String> data = new ArrayList<>();
private LocalDateTime createdAt;
public ReportGenerator() {
this.reportId = "REPORT-" + System.currentTimeMillis();
this.createdAt = LocalDateTime.now();
System.out.println("Nueva instancia ReportGenerator creada: " + reportId);
}
public void addData(String item) {
data.add(item);
}
public String getReport() {
return String.format("Report %s creado a %s con %d items",
reportId, createdAt, data.size());
}
}
Puntos clave:
@Scope("prototype")→ Nueva instancia en cada inyección- Cada instancia tiene su propio
reportIdydata
Paso 1.5: Probar el Prototype
Agregue en TestController.java:
@RestController
@RequestMapping("/api/test")
public class TestController {
private final CounterService counterService;
private final ApplicationContext context; // ← Para obtener Prototypes
public TestController(CounterService counterService,
ApplicationContext context) {
this.counterService = counterService;
this.context = context;
}
// ... método singleton ...
@GetMapping("/prototype")
public String testPrototype() {
// Obtener 2 instancias
ReportGenerator report1 = context.getBean(ReportGenerator.class);
ReportGenerator report2 = context.getBean(ReportGenerator.class);
// Modificar report1
report1.addData("Item 1");
report1.addData("Item 2");
// Verificar ambos
return "Report1: " + report1.getReport() + "<br>" +
"Report2: " + report2.getReport() + "<br>" +
"(Son instancias DIFERENTES)";
}
}
Pruebe:
http://localhost:8080/api/test/prototype
Resultado:
Report1: Report REPORT-1234567890 creado a 2025-11-28T10:00:00 con 2 items
Report2: Report REPORT-1234567891 creado a 2025-11-28T10:00:01 con 0 items
✅ ¡Instancias DIFERENTES! Cada una tiene su propio estado.
Ejercicio 2: Ciclo de Vida con @PostConstruct y @PreDestroy
Objetivo
Ejecutar código después de la inyección y antes de la destrucción.
Paso 2.1: Crear CacheService con Hooks de Ciclo de Vida
Archivo: service/CacheService.java
package com.example.beans.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class CacheService {
private Map<String, String> cache;
// Constructor
public CacheService() {
System.out.println("1. Constructor CacheService llamado");
// ⚠️ ¡No inicializar aquí si depende de otros beans!
}
// Llamado DESPUÉS de la inyección de todas las dependencias
@PostConstruct
public void init() {
System.out.println("2. @PostConstruct - Inicialización del cache");
cache = new HashMap<>();
cache.put("key1", "value1");
cache.put("key2", "value2");
System.out.println(" Cache inicializado con " + cache.size() + " elementos");
}
// Llamado ANTES de la destrucción del bean
@PreDestroy
public void cleanup() {
System.out.println("3. @PreDestroy - Limpieza del cache");
if (cache != null) {
System.out.println(" Guardando " + cache.size() + " elementos");
cache.clear();
}
System.out.println(" Cache limpiado");
}
public void put(String key, String value) {
cache.put(key, value);
System.out.println("Agregado al cache: " + key + " = " + value);
}
public String get(String key) {
return cache.get(key);
}
public int size() {
return cache.size();
}
}
Paso 2.2: Probar
Agregue en TestController.java:
private final CacheService cacheService;
@GetMapping("/lifecycle")
public String testLifecycle() {
cacheService.put("test", "valor");
return "Cache size: " + cacheService.size() + " - Ver consola para el ciclo de vida";
}
Inicie la aplicación:
Consola al iniciar:
1. Constructor CacheService llamado
2. @PostConstruct - Inicialización del cache
Cache inicializado con 2 elementos
Pruebe:
http://localhost:8080/api/test/lifecycle
Consola:
Agregado al cache: test = valor
Detenga la aplicación (Ctrl+C):
Consola al detener:
3. @PreDestroy - Limpieza del cache
Guardando 3 elementos
Cache limpiado
✅ ¡Ciclo de vida completo probado!
Ejercicio 3: Crear Beans con @Bean y @Configuration
Objetivo
Crear Beans para clases que usted no controla (bibliotecas externas).
Paso 3.1: Crear la Configuración
Archivo: config/AppConfig.java
package com.example.beans.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.format.DateTimeFormatter;
import java.util.Random;
@Configuration
public class AppConfig {
// Bean 1: DateTimeFormatter (clase Java estándar)
@Bean
public DateTimeFormatter dateFormatter() {
System.out.println("Creación del Bean dateFormatter");
return DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
}
// Bean 2: Random (clase Java estándar)
@Bean
public Random randomGenerator() {
System.out.println("Creación del Bean randomGenerator");
return new Random();
}
// Bean 3: Bean personalizado con dependencia
@Bean
public ReportFormatter reportFormatter(DateTimeFormatter dateFormatter) {
System.out.println("Creación del Bean reportFormatter");
// ¡Spring inyecta automáticamente dateFormatter!
return new ReportFormatter(dateFormatter);
}
}
Paso 3.2: Crear ReportFormatter
Archivo: service/ReportFormatter.java
package com.example.beans.service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ReportFormatter {
private final DateTimeFormatter dateFormatter;
public ReportFormatter(DateTimeFormatter dateFormatter) {
this.dateFormatter = dateFormatter;
}
public String formatReport(String title, String content) {
String timestamp = LocalDateTime.now().format(dateFormatter);
return String.format("=== %s ===\nDate: %s\n%s",
title, timestamp, content);
}
}
Paso 3.3: Probar
Agregue en TestController.java:
private final Random random;
private final ReportFormatter reportFormatter;
@GetMapping("/config-beans")
public String testConfigBeans() {
int randomNumber = random.nextInt(100);
String report = reportFormatter.formatReport(
"Test Report",
"Número aleatorio: " + randomNumber
);
return report.replace("\n", "<br>");
}
Pruebe:
http://localhost:8080/api/test/config-beans
Resultado:
=== Test Report ===
Date: 28/11/2025 10:30:45
Número aleatorio: 42
✅ ¡Beans creados vía @Configuration funcionan!
Ejercicio 4: Beans Condicionales (Bonus)
Objetivo
Crear Beans solo en ciertas condiciones.
Paso 4.1: Bean Condicional Simple
Agregue en AppConfig.java:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Configuration
public class AppConfig {
// ... otros beans ...
// Bean creado solo si la propiedad está definida
@Bean
@ConditionalOnProperty(name = "app.feature.advanced", havingValue = "true")
public String advancedFeature() {
return "¡Funcionalidad Avanzada Activada!";
}
}
Paso 4.2: Activar la Funcionalidad
Archivo: src/main/resources/application.properties
app.feature.advanced=true
Paso 4.3: Probar
Agregue en TestController.java:
import org.springframework.beans.factory.annotation.Autowired;
@RestController
@RequestMapping("/api/test")
public class TestController {
@Autowired(required = false) // ← ¡Opcional!
private String advancedFeature;
@GetMapping("/conditional")
public String testConditional() {
if (advancedFeature != null) {
return "✅ " + advancedFeature;
} else {
return "❌ Funcionalidad avanzada desactivada";
}
}
}
Pruebe con app.feature.advanced=true:
✅ ¡Funcionalidad Avanzada Activada!
Pruebe con app.feature.advanced=false:
❌ Funcionalidad avanzada desactivada
Resumen
Lo que Ha Aprendido
| Concepto | Sintaxis | Uso |
|---|---|---|
| Singleton | @Service (por defecto) | Servicios sin estado, 1 instancia |
| Prototype | @Scope("prototype") | Objetos con estado, N instancias |
| @PostConstruct | Método anotado | Inicialización después de la inyección |
| @PreDestroy | Método anotado | Limpieza antes de la destrucción |
| @Bean | En @Configuration | Clases externas, configuración compleja |
| Bean condicional | @ConditionalOnProperty | Beans según configuración |
Comparación Singleton vs Prototype
// Singleton (por defecto)
@Service
public class SingletonService {
private int state = 0; // ⚠️ ¡Compartido!
}
// Prototype
@Service
@Scope("prototype")
public class PrototypeService {
private int state = 0; // ✅ Propio de cada instancia
}
¿Cuándo Usar Cada Uno?
Singleton ✅ (por defecto):
- Servicios sin estado
- Repositories
- Controllers
- ¡99% de los casos!
Prototype ⚠️ (raro):
- Objetos con estado mutable
- Objetos temporales
- Reportes, procesadores de archivos
Autoevaluación
- ¿Cuál es la diferencia entre Singleton y Prototype?
- ¿Cuándo se llaman @PostConstruct y @PreDestroy?
- ¿Por qué usar @Bean en lugar de @Service?
- ¿Cómo crear un Bean condicional?
Desafío Bonus 🎯
Cree un sistema de cache con TTL (Time To Live):
@Service @Scope("singleton")CacheManager- Método
put(key, value, ttlSeconds) - Método
get(key)que retorna null si está expirado @PostConstructpara inicializar@PreDestroypara guardar el cache
Bonus: Utilice @Scheduled para limpiar automáticamente las entradas expiradas.
Próximo Paso
¡Pase al TP3 para aprender la configuración por perfiles!
➡️ TP5.3: Configuración por Perfiles