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
- Los ámbitos de los Beans (Singleton, Prototype, etc.)
- Cuándo usar cada ámbito
- Configuración con @Configuration
- Los perfiles (@Profile) para dev/test/prod
- @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
| Ámbito | Descripción | Instancias | Uso |
|---|---|---|---|
@Singleton | 1 instancia para toda la aplicación | 1 | Por defecto - Services, Repositories |
@Prototype | Nueva instancia en cada inyección | Varias | Objetos con estado |
@RequestScope | 1 instancia por petición HTTP | 1/petición | Objetos ligados a una petición |
@SessionScope | 1 instancia por sesión de usuario | 1/sesión | Carrito, preferencias de usuario |
@ApplicationScope | 1 instancia para la aplicación web | 1 | Similar 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
| Ámbito | Instancias | Duración de Vida | Uso Principal |
|---|---|---|---|
| Singleton | 1 para la app | Inicio → Parada | Services, Repos (por defecto) |
| Prototype | 1 por obtención | Creación → GC | Objetos con estado |
| Request | 1 por petición HTTP | Petición → Respuesta | Logs, contexto de petición |
| Session | 1 por sesión de usuario | Inicio de sesión → fin | Carrito, 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
- ¿Cuál es la diferencia entre Singleton y Prototype?
- ¿Cuándo usar RequestScope?
- ¿Cómo activar un perfil Spring?
- ¿Cuál es la diferencia entre application.properties y application-dev.properties?
- ¿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!