Saltar al contenido principal

TP5.2 - Beans y Ámbitos

Objetivos​

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

  1. ✅ Crear Beans con @Bean y @Configuration
  2. ✅ Comprender la diferencia entre Singleton y Prototype
  3. ✅ Utilizar @PostConstruct y @PreDestroy
  4. ✅ 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 defecto
  • counter compartido 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 reportId y data

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​

ConceptoSintaxisUso
Singleton@Service (por defecto)Servicios sin estado, 1 instancia
Prototype@Scope("prototype")Objetos con estado, N instancias
@PostConstructMétodo anotadoInicialización después de la inyección
@PreDestroyMétodo anotadoLimpieza antes de la destrucción
@BeanEn @ConfigurationClases externas, configuración compleja
Bean condicional@ConditionalOnPropertyBeans 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​

  1. ¿Cuál es la diferencia entre Singleton y Prototype?
  2. ¿Cuándo se llaman @PostConstruct y @PreDestroy?
  3. ¿Por qué usar @Bean en lugar de @Service?
  4. ¿Cómo crear un Bean condicional?

Desafío Bonus 🎯​

Cree un sistema de cache con TTL (Time To Live):

  1. @Service @Scope("singleton") CacheManager
  2. Método put(key, value, ttlSeconds)
  3. Método get(key) que retorna null si está expirado
  4. @PostConstruct para inicializar
  5. @PreDestroy para 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

⬅️ TP anterior: Inyección Avanzada

⬅️ Volver al Módulo 5