Skip to content

4. Gestión de Beans y Anotaciones

🏷️ 4.1 @Component, @Service, @Repository, @Controller

Section titled “🏷️ 4.1 @Component, @Service, @Repository, @Controller”

Spring proporciona anotaciones especializadas para marcar clases como beans según su rol en la arquitectura:

Anotaciones de estereotipo
// @Component - Bean genérico
@Component
public class UtilHelper {
public String formatear(String texto) {
return texto.trim().toUpperCase();
}
}
// @Service - Capa de lógica de negocio
@Service
public class UsuarioService {
public Usuario crearUsuario(String nombre) {
// Lógica de negocio
return new Usuario(nombre);
}
}
// @Repository - Capa de acceso a datos
@Repository
public class UsuarioRepository {
public Usuario findById(Long id) {
// Acceso a base de datos
return entityManager.find(Usuario.class, id);
}
}
// @Controller - Controlador MVC (retorna vistas)
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("mensaje", "Bienvenido");
return "home"; // Retorna nombre de vista
}
}
// @RestController - Controlador REST (retorna JSON)
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
@GetMapping
public List<Usuario> listar() {
return usuarioService.listarTodos(); // Retorna JSON
}
}
AnotaciónCapaCaracterísticas especiales
@ComponentGenéricaNinguna
@ServiceNegocioSemántica de servicio
@RepositoryDatosTraducción de excepciones SQL
@ControllerWebIntegración con MVC
@RestControllerAPI@Controller + @ResponseBody

🔗 4.2 @Autowired y Resolución de Dependencias

Section titled “🔗 4.2 @Autowired y Resolución de Dependencias”
@Autowired por constructor
@Service
public class PedidoService {
// Inyección por constructor (recomendada)
private final ProductoRepository productoRepo;
private final ClienteRepository clienteRepo;
@Autowired // Opcional si hay un solo constructor
public PedidoService(ProductoRepository productoRepo,
ClienteRepository clienteRepo) {
this.productoRepo = productoRepo;
this.clienteRepo = clienteRepo;
}
public Pedido crearPedido(Long clienteId, List<Long> productosIds) {
Cliente cliente = clienteRepo.findById(clienteId).orElseThrow();
List<Producto> productos = productoRepo.findAllById(productosIds);
return new Pedido(cliente, productos);
}
}
Resolución automática de dependencias
// Spring busca un bean que coincida con el tipo
@Service
public class NotificacionService {
// Spring busca un bean de tipo EmailSender
private final EmailSender emailSender;
public NotificacionService(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
// Si hay UNA implementación, Spring la inyecta automáticamente
@Component
public class GmailEmailSender implements EmailSender {
@Override
public void enviar(String destinatario, String mensaje) {
// Implementación con Gmail
}
}
Dependencia opcional
@Service
public class ReporteService {
private final ReporteRepository repository;
private PdfGenerator pdfGenerator; // Opcional
public ReporteService(ReporteRepository repository) {
this.repository = repository;
}
// Dependencia opcional con setter
@Autowired(required = false)
public void setPdfGenerator(PdfGenerator pdfGenerator) {
this.pdfGenerator = pdfGenerator;
}
public byte[] generarReporte(Long id) {
Reporte reporte = repository.findById(id).orElseThrow();
if (pdfGenerator != null) {
return pdfGenerator.generar(reporte);
}
return reporte.toString().getBytes();
}
}

Problema de ambigüedad
// Interfaz con múltiples implementaciones
public interface MensajeService {
void enviar(String mensaje);
}
@Service
public class EmailMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando email: " + mensaje);
}
}
@Service
public class SmsMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando SMS: " + mensaje);
}
}
// ❌ ERROR: Spring no sabe cuál inyectar
@Service
public class NotificacionService {
private final MensajeService mensajeService; // ¿Cuál?
public NotificacionService(MensajeService mensajeService) {
this.mensajeService = mensajeService;
}
}
Solución con @Qualifier
@Service("emailService") // Nombre explícito del bean
public class EmailMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando email: " + mensaje);
}
}
@Service("smsService")
public class SmsMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando SMS: " + mensaje);
}
}
// ✅ Especificar cuál queremos con @Qualifier
@Service
public class NotificacionService {
private final MensajeService emailService;
private final MensajeService smsService;
public NotificacionService(
@Qualifier("emailService") MensajeService emailService,
@Qualifier("smsService") MensajeService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}
Solución con @Primary
@Service
@Primary // Este será el bean por defecto
public class EmailMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando email: " + mensaje);
}
}
@Service
public class SmsMensajeService implements MensajeService {
@Override
public void enviar(String mensaje) {
System.out.println("Enviando SMS: " + mensaje);
}
}
// ✅ Spring inyecta EmailMensajeService (el @Primary)
@Service
public class NotificacionService {
private final MensajeService mensajeService;
public NotificacionService(MensajeService mensajeService) {
this.mensajeService = mensajeService; // EmailMensajeService
}
}

📦 4.4 @Scope (singleton, prototype, request, session)

Section titled “📦 4.4 @Scope (singleton, prototype, request, session)”
ScopeDescripciónInstancias
singletonUna instancia por contenedor (default)1
prototypeNueva instancia cada vez que se solicitaN
requestUna instancia por request HTTP1 por request
sessionUna instancia por sesión HTTP1 por sesión
Scope singleton
@Service
@Scope("singleton") // Opcional, es el default
public class ConfiguracionService {
private Map<String, String> configuraciones = new HashMap<>();
// Todos los componentes comparten la misma instancia
public String getConfiguracion(String clave) {
return configuraciones.get(clave);
}
}
Scope prototype
@Component
@Scope("prototype") // Nueva instancia cada vez
public class Carrito {
private List<Producto> productos = new ArrayList<>();
public void agregarProducto(Producto producto) {
productos.add(producto);
}
public BigDecimal getTotal() {
return productos.stream()
.map(Producto::getPrecio)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// Cada inyección crea una nueva instancia
@RestController
public class CompraController {
@Autowired
private ApplicationContext context;
@PostMapping("/nueva-compra")
public void nuevaCompra() {
// Obtener nueva instancia de Carrito
Carrito carrito = context.getBean(Carrito.class);
}
}
Scope request y session
// Una instancia por cada request HTTP
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestInfo {
private LocalDateTime timestamp = LocalDateTime.now();
private String requestId = UUID.randomUUID().toString();
}
// Una instancia por cada sesión de usuario
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CarritoCompras {
private List<Producto> items = new ArrayList<>();
public void agregar(Producto producto) {
items.add(producto);
}
}

🔄 4.5 Inicialización y Destrucción de Beans

Section titled “🔄 4.5 Inicialización y Destrucción de Beans”
@PostConstruct y @PreDestroy
@Service
public class CacheService {
private Map<String, Object> cache;
// Se ejecuta DESPUÉS de la inyección de dependencias
@PostConstruct
public void inicializar() {
System.out.println("Inicializando cache...");
this.cache = new ConcurrentHashMap<>();
cargarDatosIniciales();
}
// Se ejecuta ANTES de destruir el bean
@PreDestroy
public void limpiar() {
System.out.println("Limpiando cache...");
this.cache.clear();
}
private void cargarDatosIniciales() {
// Cargar datos desde BD o archivo
}
}
initMethod y destroyMethod
@Configuration
public class AppConfig {
@Bean(initMethod = "iniciar", destroyMethod = "cerrar")
public ConexionPool conexionPool() {
return new ConexionPool();
}
}
public class ConexionPool {
public void iniciar() {
System.out.println("Creando pool de conexiones...");
}
public void cerrar() {
System.out.println("Cerrando conexiones...");
}
}

Gestión de recursos con ciclo de vida
@Service
public class ReporteScheduler {
private ScheduledExecutorService executor;
@PostConstruct
public void iniciar() {
// Iniciar tareas programadas
executor = Executors.newScheduledThreadPool(2);
executor.scheduleAtFixedRate(
this::generarReporteDiario,
0, 24, TimeUnit.HOURS
);
}
@PreDestroy
public void detener() {
// Detener tareas y liberar recursos
if (executor != null) {
executor.shutdown();
try {
executor.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
private void generarReporteDiario() {
// Lógica del reporte
}
}

Beans con @Profile
// Bean solo para desarrollo
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
@Override
public void enviar(String destinatario, String mensaje) {
System.out.println("DEV - Email simulado a: " + destinatario);
}
}
// Bean solo para producción
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
@Override
public void enviar(String destinatario, String mensaje) {
// Envío real por SMTP
smtpClient.send(destinatario, mensaje);
}
}
// Bean para múltiples perfiles
@Service
@Profile({"dev", "test"})
public class H2DatabaseConfig {
// Configuración de H2 para dev y test
}
Activar perfiles
# application.properties
spring.profiles.active=dev
# application.yml
spring:
profiles:
active: dev
# Por línea de comandos
java -jar app.jar --spring.profiles.active=prod
# Variables de entorno
export SPRING_PROFILES_ACTIVE=prod
Archivos de configuración por perfil
# application-dev.properties (perfil dev)
server.port=8080
logging.level.root=DEBUG
spring.datasource.url=jdbc:h2:mem:testdb
# application-prod.properties (perfil prod)
server.port=80
logging.level.root=WARN
spring.datasource.url=jdbc:postgresql://prod-server:5432/mydb

⚙️ 4.8 Configuración Condicional de Beans

Section titled “⚙️ 4.8 Configuración Condicional de Beans”
@Conditional personalizado
// Condición personalizada
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
return context.getEnvironment()
.getProperty("os.name")
.toLowerCase()
.contains("windows");
}
}
@Configuration
public class FileConfig {
@Bean
@Conditional(WindowsCondition.class)
public FileService windowsFileService() {
return new WindowsFileService();
}
}
Anotaciones @ConditionalOn...
@Configuration
public class ConditionalConfig {
// Solo si existe la clase en el classpath
@Bean
@ConditionalOnClass(DataSource.class)
public DatabaseHealthCheck dbHealthCheck() {
return new DatabaseHealthCheck();
}
// Solo si NO existe un bean de este tipo
@Bean
@ConditionalOnMissingBean(EmailService.class)
public EmailService defaultEmailService() {
return new SimpleEmailService();
}
// Solo si la propiedad tiene cierto valor
@Bean
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
public CacheService cacheService() {
return new RedisCacheService();
}
// Solo en entorno web
@Bean
@ConditionalOnWebApplication
public SessionManager sessionManager() {
return new HttpSessionManager();
}
}
AnotaciónCondición
@ConditionalOnClassClase existe en classpath
@ConditionalOnMissingClassClase NO existe
@ConditionalOnBeanBean existe en contexto
@ConditionalOnMissingBeanBean NO existe
@ConditionalOnPropertyPropiedad tiene valor
@ConditionalOnWebApplicationEs aplicación web
@ConditionalOnExpressionExpresión SpEL es true
🐝