Saltar al contenido principal

Capítulo 4: Ámbitos y Configuración

Introducción​

Usted sabe crear Beans, pero ¿sabe cuántas instancias crea Spring? ¿Una sola para toda la aplicación? ¿Una nueva cada vez?

Este capítulo le enseña a controlar el ciclo de vida de los Beans con los Ámbitos.

Lo que va a aprender​

  1. Los ámbitos de los Beans (Singleton, Prototype, etc.)
  2. Cuándo usar cada ámbito
  3. Configuración con @Configuration
  4. Los perfiles (@Profile) para dev/test/prod
  5. @ComponentScan

Duración: 45 minutos


Los Ámbitos de los Beans​

¿Qué es un Ámbito?​

Ámbito = Duración de vida y número de instancias de un Bean

Pregunta: Cuando Spring crea UserService, ¿cuántas instancias crea?

  • ¿Una sola para toda la aplicación? (Singleton)
  • ¿Una nueva en cada inyección? (Prototype)
  • ¿Una por cada petición HTTP? (Request)
  • ¿Una por sesión de usuario? (Session)

¡El ámbito determina la respuesta!


Los 5 Ámbitos Disponibles​

ÁmbitoDescripciónInstanciasUso
@Singleton1 instancia para toda la aplicación1Por defecto - Services, Repositories
@PrototypeNueva instancia en cada inyecciónVariasObjetos con estado
@RequestScope1 instancia por petición HTTP1/peticiónObjetos ligados a una petición
@SessionScope1 instancia por sesión de usuario1/sesiónCarrito, preferencias de usuario
@ApplicationScope1 instancia para la aplicación web1Similar a Singleton para web

1. Ámbito Singleton (Por defecto)​

Principio​

Spring crea UNA SOLA instancia del Bean
Todos los componentes comparten la MISMA instancia

Código​

@Service  // Singleton por defecto
public class UserService {

private int counter = 0;

public void incrementCounter() {
counter++;
}

public int getCounter() {
return counter;
}
}

Prueba​

@RestController
public class TestController {

private final UserService userService;

public TestController(UserService userService) {
this.userService = userService;
}

@GetMapping("/test")
public String test() {
userService.incrementCounter();
return "Counter: " + userService.getCounter();
}
}

Resultado:

Requisición 1: Counter: 1
Requisición 2: Counter: 2
Requisición 3: Counter: 3
...

¿Por qué? ¡Es la MISMA instancia para todas las peticiones!


Visualización​

¡Todos comparten la misma instancia!


¿Cuándo Usar Singleton?​

✅ Servicios sin estado (stateless)

@Service
public class EmailService {
public void send(String to, String message) {
// No hay variables de instancia modificables
// Solo métodos que trabajan con los parámetros
}
}

✅ Repositories

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Sin estado
}

✅ Controllers

@RestController
public class UserController {
// Sin estado (las dependencias son final)
}

Regla: ¡Si el Bean no tiene estado mutable, use Singleton (por defecto)!


2. Ámbito Prototype​

Principio​

Spring crea una NUEVA instancia cada vez que se solicita el Bean
Cada componente tiene su PROPIA instancia

Código​

import org.springframework.context.annotation.Scope;

@Service
@Scope("prototype") // ← Nueva instancia cada vez
public class ShoppingCart {

private List<Product> items = new ArrayList<>();

public void addProduct(Product product) {
items.add(product);
}

public List<Product> getItems() {
return items;
}
}

Prueba​

@RestController
public class CartController {

private final ApplicationContext context;

public CartController(ApplicationContext context) {
this.context = context;
}

@GetMapping("/test-prototype")
public String testPrototype() {
// Obtener 2 instancias
ShoppingCart cart1 = context.getBean(ShoppingCart.class);
ShoppingCart cart2 = context.getBean(ShoppingCart.class);

// Modificar cart1
cart1.addProduct(new Product("Laptop"));

// Verificar cart2
return "Cart1: " + cart1.getItems().size() +
", Cart2: " + cart2.getItems().size();
}
}

Resultado:

Cart1: 1, Cart2: 0

¿Por qué? ¡Son instancias DIFERENTES!


Visualización​

¡Cada componente tiene su propia instancia!


¿Cuándo Usar Prototype?​

✅ Objetos con estado

@Scope("prototype")
public class ReportGenerator {
private String title;
private List<Data> data;
// Estado específico de cada reporte
}

✅ Objetos temporales

@Scope("prototype")
public class FileProcessor {
private File currentFile;
// Procesamiento de un archivo específico
}

⚠️ Atención: ¡Prototype con Singleton = problema!


Problema: Prototype en Singleton​

@Service  // Singleton
public class OrderService {

private final ShoppingCart cart; // Prototype

// ❌ ¡PROBLEMA!
public OrderService(ShoppingCart cart) {
this.cart = cart;
// cart se inyecta SOLO UNA VEZ (al inicio)
// ¡Aunque ShoppingCart sea Prototype!
}
}

Resultado: ¡El Prototype se comporta como un Singleton!

Solución: Utilizar ObjectProvider o ApplicationContext.getBean() para obtener una nueva instancia cada vez.


3. Ámbito Request (Web)​

Principio​

1 nueva instancia POR petición HTTP
La instancia existe durante toda la petición
Se destruye después de la respuesta

Código​

import org.springframework.web.context.annotation.RequestScope;

@Component
@RequestScope // ← Nueva instancia por petición
public class RequestLogger {

private String requestId;
private LocalDateTime startTime;

@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = LocalDateTime.now();
System.out.println("Petición " + requestId + " iniciada");
}

@PreDestroy
public void cleanup() {
Duration duration = Duration.between(startTime, LocalDateTime.now());
System.out.println("Petición " + requestId + " terminada en " +
duration.toMillis() + "ms");
}
}

Uso​

@RestController
public class UserController {

private final RequestLogger logger;

public UserController(RequestLogger logger) {
this.logger = logger;
}

@GetMapping("/users")
public List<User> getUsers() {
logger.log("Recuperación de usuarios");
// ... lógica
}
}

Resultado:

Petición 1:
Petición abc-123 iniciada
Petición abc-123 terminada en 45ms

Petición 2:
Petición def-456 iniciada
Petición def-456 terminada en 32ms

¡Cada petición tiene su propia instancia!


4. Ámbito Session (Web)​

Principio​

1 instancia por sesión de usuario
Compartida entre todas las peticiones de un mismo usuario
Se destruye cuando la sesión expira

Código​

import org.springframework.web.context.annotation.SessionScope;

@Component
@SessionScope // ← Instancia por sesión de usuario
public class UserPreferences {

private String theme = "light";
private String language = "fr";

public void setTheme(String theme) {
this.theme = theme;
}

public String getTheme() {
return theme;
}
}

Uso: Carrito de compras, preferencias, autenticación


Comparación de Ámbitos​

Tabla Resumen​

ÁmbitoInstanciasDuración de VidaUso Principal
Singleton1 para la appInicio → ParadaServices, Repos (por defecto)
Prototype1 por obtenciónCreación → GCObjetos con estado
Request1 por petición HTTPPetición → RespuestaLogs, contexto de petición
Session1 por sesión de usuarioInicio de sesión → finCarrito, preferencias

Visualización Temporal​

Singleton:     [════════════════════════════════] (toda la app)

Prototype: [===] [===] [===] [===] [===] (a demanda)

Request: [req1] [req2] [req3] [req4] (por petición)

Session: [═══ Sesión User1 ═══]
[═══════ Sesión User2 ═══════]

Configuración con @Configuration​

Clase de Configuración Completa​

@Configuration
public class AppConfig {

// Bean Singleton (por defecto)
@Bean
public EmailService emailService() {
EmailService service = new EmailService();
service.setHost("smtp.gmail.com");
return service;
}

// Bean Prototype
@Bean
@Scope("prototype")
public ReportGenerator reportGenerator() {
return new ReportGenerator();
}

// Bean condicional (explicado más abajo)
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new H2DataSource();
}

@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new PostgreSQLDataSource();
}
}

Los Perfiles (@Profile)​

Problema​

Usted tiene diferentes configuraciones según el entorno:

Desarrollo:
- Base H2 en memoria
- Logs detallados
- Email en modo fake

Producción:
- Base PostgreSQL
- Logs mínimos
- Email real

¿Cómo gestionar esto de forma limpia?


Solución: Perfiles Spring​

Crear Beans según el perfil activo:

@Configuration
public class DatabaseConfig {

@Bean
@Profile("dev") // ← Activo solo en dev
public DataSource devDataSource() {
System.out.println("Uso de H2 (dev)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.build();
}

@Bean
@Profile("prod") // ← Activo solo en prod
public DataSource prodDataSource() {
System.out.println("Uso de PostgreSQL (prod)");
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("admin");
config.setPassword("secret");
return new HikariDataSource(config);
}

@Bean
@Profile("test") // ← Activo solo en test
public DataSource testDataSource() {
System.out.println("Uso de H2 (test)");
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("test-schema.sql")
.build();
}
}

Activar un Perfil​

Método 1: application.properties

spring.profiles.active=dev

Método 2: Variable de entorno

export SPRING_PROFILES_ACTIVE=prod

Método 3: Argumento JVM

java -jar app.jar --spring.profiles.active=prod

Método 4: IDE (VS Code, IntelliJ)

"spring.profiles.active": "dev"

Perfiles Múltiples​

Activar varios perfiles:

spring.profiles.active=dev,debug,mock-email

Bean activo para varios perfiles:

@Bean
@Profile({"dev", "test"}) // ← Activo en dev O test
public DataSource devOrTestDataSource() {
return new H2DataSource();
}

Ejemplo Completo: Configuración por Entorno​

// Interfaz común
public interface EmailService {
void send(String to, String message);
}

// Implementación DEV (fake)
@Service
@Profile("dev")
public class FakeEmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("FAKE EMAIL to " + to + ": " + message);
}
}

// Implementación PROD (real)
@Service
@Profile("prod")
public class RealEmailService implements EmailService {
@Override
public void send(String to, String message) {
// Lógica real de envío vía SMTP
smtpClient.send(to, message);
}
}

En dev: No se envían emails reales
En prod: Se envían emails reales


Archivos de Configuración por Perfil​

Estructura​

src/main/resources/
├── application.properties (común)
├── application-dev.properties (solo dev)
├── application-test.properties (solo test)
└── application-prod.properties (solo prod)

Ejemplo​

application.properties (común):

app.name=My Application
server.port=8080

application-dev.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.show-sql=true
logging.level.root=DEBUG

application-prod.properties:

spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
logging.level.root=WARN

Activación:

spring.profiles.active=dev

¡Spring carga automáticamente application-dev.properties!


@ComponentScan​

Problema​

Por defecto, Spring solo escanea el paquete de la clase principal y sus subpaquetes.

com.example.demo
├── DemoApplication.java ← Clase principal
├── service/
│ └── UserService.java ✅ Escaneado
└── controller/
└── UserController.java ✅ Escaneado

com.external.library
└── ExternalService.java ❌ NO escaneado

Solución: @ComponentScan​

@SpringBootApplication
@ComponentScan(basePackages = {
"com.example.demo", // Paquete principal
"com.external.library" // Paquete externo
})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

¡Ahora ExternalService será escaneado!


Filtrar el Scan​

Excluir ciertas clases:

@ComponentScan(
basePackages = "com.example.demo",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {OldService.class, DeprecatedController.class}
)
)

Incluir solo ciertas anotaciones:

@ComponentScan(
basePackages = "com.example.demo",
includeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Service.class // Solo @Service
),
useDefaultFilters = false
)

Ejemplo Completo: Aplicación Multi-Entornos​

// Clase principal
@SpringBootApplication
public class ECommerceApplication {
public static void main(String[] args) {
SpringApplication.run(ECommerceApplication.class, args);
}
}

// Configuración
@Configuration
public class AppConfig {

// Email según entorno
@Bean
@Profile("dev")
public EmailService devEmailService() {
return new FakeEmailService();
}

@Bean
@Profile("prod")
public EmailService prodEmailService() {
return new SmtpEmailService();
}

// Cache según entorno
@Bean
@Profile("prod")
@Scope("singleton")
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}

@Bean
@Profile("dev")
@Scope("singleton")
public CacheManager simpleCacheManager() {
return new SimpleCacheManager();
}
}

// Servicio con scope apropiado
@Service
@Scope("singleton") // Una instancia para la app
public class OrderService {

private final EmailService emailService;
private final CacheManager cacheManager;

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

// Bean con estado (Prototype)
@Component
@Scope("prototype") // Nueva instancia cada vez
public class Invoice {
private Order order;
private BigDecimal total;

public void generate(Order order) {
this.order = order;
this.total = calculateTotal(order);
}
}

Resumen del Capítulo​

Ámbitos​

@Scope("singleton")   // 1 instancia (por defecto)
@Scope("prototype") // N instancias
@RequestScope // 1 por petición HTTP
@SessionScope // 1 por sesión user

Perfiles​

@Profile("dev")       // Activo en dev
@Profile("prod") // Activo en prod
@Profile({"dev","test"}) // Activo en dev O test

Activación:

spring.profiles.active=dev

Configuración​

@Configuration        // Clase de configuración
@Bean // Crear un Bean
@ComponentScan // Escanear paquetes

Puntos Clave​

✅ Singleton por defecto (servicios sin estado)
✅ Prototype para objetos con estado
✅ Request/Session para web
✅ @Profile para gestionar dev/test/prod
✅ application-{profile}.properties para configuración
✅ @ComponentScan para paquetes externos


Autoevaluación​

  1. ¿Cuál es la diferencia entre Singleton y Prototype?
  2. ¿Cuándo usar RequestScope?
  3. ¿Cómo activar un perfil Spring?
  4. ¿Cuál es la diferencia entre application.properties y application-dev.properties?
  5. ¿Para qué sirve @ComponentScan?

¡Felicidades! 🎉​

¡Ha completado toda la teoría del Módulo 5!

Ahora comprende:

  • ✅ La Inversión de Control (IoC)
  • ✅ La Inyección de Dependencias (DI)
  • ✅ Los Beans de Spring
  • ✅ Los Ámbitos y la Configuración

Próximo paso: ¡Poner en práctica con los TPs!


➡️ Comenzar TP1: Inyección Avanzada

⬅️ Capítulo anterior: Beans

⬅️ Volver al Módulo 5