12. Programación Orientada a Aspectos (AOP)
🎯 12.1 ¿Qué es AOP?
Section titled “🎯 12.1 ¿Qué es AOP?”Definición
Section titled “Definición”AOP (Aspect-Oriented Programming) es un paradigma que permite separar las preocupaciones transversales (cross-cutting concerns) del código de negocio principal.
Preocupaciones transversales
Section titled “Preocupaciones transversales”// Sin AOP: código repetido en muchos lugares@Servicepublic class UsuarioService {
public Usuario crear(UsuarioDTO dto) { long inicio = System.currentTimeMillis(); // Logging log.info("Iniciando crear usuario"); // Logging
try { // Lógica de negocio Usuario usuario = new Usuario(dto); return repository.save(usuario); } catch (Exception e) { log.error("Error: " + e.getMessage()); // Logging throw e; } finally { long tiempo = System.currentTimeMillis() - inicio; log.info("Tiempo: " + tiempo + "ms"); // Logging } }}
// Con AOP: código limpio, logging separado@Servicepublic class UsuarioService {
public Usuario crear(UsuarioDTO dto) { Usuario usuario = new Usuario(dto); return repository.save(usuario); }}Casos de uso comunes
Section titled “Casos de uso comunes”- Logging: Registrar entradas/salidas de métodos
- Seguridad: Verificar permisos
- Transacciones:
@Transactionalusa AOP internamente - Caché:
@Cacheableusa AOP - Métricas: Medir tiempos de ejecución
- Validación: Verificar parámetros
📚 12.2 Conceptos Fundamentales
Section titled “📚 12.2 Conceptos Fundamentales”Terminología AOP
Section titled “Terminología AOP”| Término | Descripción |
|---|---|
| Aspect | Módulo que encapsula una preocupación transversal |
| Join Point | Punto de ejecución (método, excepción, etc.) |
| Advice | Acción a ejecutar en un Join Point |
| Pointcut | Expresión que selecciona Join Points |
| Target | Objeto siendo “aspectado” |
| Weaving | Proceso de aplicar aspectos al código |
Diagrama conceptual
Section titled “Diagrama conceptual”┌─────────────────────────────────────────────────────┐│ ASPECTO ││ ┌─────────────┐ ┌─────────────────────────────┐ ││ │ Pointcut │───▶│ Advice │ ││ │ "Dónde" │ │ "Qué hacer" │ ││ └─────────────┘ └─────────────────────────────┘ │└─────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────┐│ CÓDIGO DE NEGOCIO ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Service A │ │ Service B │ │ Service C │ ││ │ método() │ │ método() │ │ método() │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ▲ ▲ ▲ ││ └────────────────┴────────────────┘ ││ Join Points │└─────────────────────────────────────────────────────┘🔧 12.3 Configuración de AOP
Section titled “🔧 12.3 Configuración de AOP”Dependencia
Section titled “Dependencia”<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>Habilitar AOP
Section titled “Habilitar AOP”@Configuration@EnableAspectJAutoProxy // Habilita AOP (incluido por defecto en Spring Boot)public class AopConfig {}Crear un aspecto básico
Section titled “Crear un aspecto básico”import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.springframework.stereotype.Component;
@Aspect@Component // Debe ser un bean de Springpublic class LoggingAspect {
@Before("execution(* com.miapp.service.*.*(..))") public void logAntesDeCadaMetodo() { System.out.println("Ejecutando método del servicio..."); }}🎯 12.4 Pointcut Expressions
Section titled “🎯 12.4 Pointcut Expressions”Sintaxis básica
Section titled “Sintaxis básica”execution(modifiers? return-type declaring-type? method-name(params) throws?)
// Ejemplos:execution(* com.miapp.service.*.*(..))│ │ │ │ │ └── Cualquier parámetro│ │ │ │ └───── Cualquier método│ │ │ └─────── Cualquier clase en service│ │ └──────────────────────── Paquete│ └────────────────────────── Cualquier tipo de retorno└──────────────────────────────────── Tipo de expresiónEjemplos de pointcuts
Section titled “Ejemplos de pointcuts”@Aspect@Componentpublic class EjemplosPointcut {
// Todos los métodos de todas las clases en service @Before("execution(* com.miapp.service.*.*(..))") public void todosLosServicios() {}
// Métodos que empiezan con "get" @Before("execution(* com.miapp.*.get*(..))") public void metodosGet() {}
// Métodos públicos que retornan Usuario @Before("execution(public Usuario com.miapp..*.*(..))") public void metodosQueRetornanUsuario() {}
// Métodos con exactamente un parámetro Long @Before("execution(* com.miapp.service.*.*(Long))") public void metodosConUnLong() {}
// Métodos con cualquier cantidad de parámetros String @Before("execution(* com.miapp.service.*.*(String, ..))") public void metodosQueComienzanConString() {}
// Todos los métodos de una clase específica @Before("execution(* com.miapp.service.UsuarioService.*(..))") public void metodosDeUsuarioService() {}
// Subpaquetes (doble punto) @Before("execution(* com.miapp..*.*(..))") public void todosLosSubpaquetes() {}}Pointcuts con anotaciones
Section titled “Pointcuts con anotaciones”// Métodos anotados con @Transactional@Before("@annotation(org.springframework.transaction.annotation.Transactional)")public void metodosTransaccionales() {}
// Clases anotadas con @Service@Before("@within(org.springframework.stereotype.Service)")public void clasesService() {}
// Métodos anotados con anotación personalizada@Before("@annotation(com.miapp.annotation.Auditable)")public void metodosAuditables() {}
// Anotación personalizada@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Auditable { String value() default "";}Combinar pointcuts
Section titled “Combinar pointcuts”@Aspect@Componentpublic class CombinedPointcuts {
// Definir pointcuts reutilizables @Pointcut("execution(* com.miapp.service.*.*(..))") public void serviceMethods() {}
@Pointcut("execution(* com.miapp.repository.*.*(..))") public void repositoryMethods() {}
// Combinar con AND @Before("serviceMethods() && args(id,..)") public void serviceMethodsConId(Long id) { System.out.println("Método con ID: " + id); }
// Combinar con OR @Before("serviceMethods() || repositoryMethods()") public void serviceORepository() {}
// Combinar con NOT @Before("serviceMethods() && !execution(* *.get*(..))") public void serviceExceptoGetters() {}}📣 12.5 Tipos de Advice
Section titled “📣 12.5 Tipos de Advice”@Before
Section titled “@Before”@Aspect@Componentpublic class BeforeAspect {
@Before("execution(* com.miapp.service.*.*(..))") public void antesDelMetodo(JoinPoint joinPoint) { String metodo = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs();
log.info("Ejecutando: {} con args: {}", metodo, Arrays.toString(args)); }}@After (finally)
Section titled “@After (finally)”@Aspect@Componentpublic class AfterAspect {
// Se ejecuta SIEMPRE, haya excepción o no @After("execution(* com.miapp.service.*.*(..))") public void despuesDelMetodo(JoinPoint joinPoint) { log.info("Método finalizado: {}", joinPoint.getSignature().getName()); }}@AfterReturning
Section titled “@AfterReturning”@Aspect@Componentpublic class AfterReturningAspect {
// Solo si el método termina exitosamente @AfterReturning( pointcut = "execution(* com.miapp.service.*.*(..))", returning = "resultado" ) public void despuesDeRetornar(JoinPoint joinPoint, Object resultado) { log.info("Método {} retornó: {}", joinPoint.getSignature().getName(), resultado); }}@AfterThrowing
Section titled “@AfterThrowing”@Aspect@Componentpublic class AfterThrowingAspect {
// Solo si el método lanza excepción @AfterThrowing( pointcut = "execution(* com.miapp.service.*.*(..))", throwing = "ex" ) public void despuesDeExcepcion(JoinPoint joinPoint, Exception ex) { log.error("Excepción en {}: {}", joinPoint.getSignature().getName(), ex.getMessage()); }}@Around (el más poderoso)
Section titled “@Around (el más poderoso)”@Aspect@Componentpublic class AroundAspect {
@Around("execution(* com.miapp.service.*.*(..))") public Object alrededorDelMetodo(ProceedingJoinPoint joinPoint) throws Throwable { String metodo = joinPoint.getSignature().getName();
// ANTES log.info("Iniciando: {}", metodo); long inicio = System.currentTimeMillis();
try { // EJECUTAR el método original Object resultado = joinPoint.proceed();
// DESPUÉS (éxito) long tiempo = System.currentTimeMillis() - inicio; log.info("Completado: {} en {}ms", metodo, tiempo);
return resultado;
} catch (Exception e) { // DESPUÉS (error) log.error("Error en {}: {}", metodo, e.getMessage()); throw e; } }}🛠️ 12.6 JoinPoint y ProceedingJoinPoint
Section titled “🛠️ 12.6 JoinPoint y ProceedingJoinPoint”JoinPoint
Section titled “JoinPoint”@Before("execution(* com.miapp.service.*.*(..))")public void infoDelMetodo(JoinPoint joinPoint) { // Nombre del método String metodo = joinPoint.getSignature().getName();
// Clase del objeto String clase = joinPoint.getTarget().getClass().getSimpleName();
// Argumentos Object[] args = joinPoint.getArgs();
// Firma completa String firma = joinPoint.getSignature().toLongString();
log.info("Clase: {}, Método: {}, Args: {}", clase, metodo, Arrays.toString(args));}ProceedingJoinPoint (solo @Around)
Section titled “ProceedingJoinPoint (solo @Around)”@Around("execution(* com.miapp.service.*.*(..))")public Object modificarEjecucion(ProceedingJoinPoint pjp) throws Throwable {
// Obtener argumentos originales Object[] args = pjp.getArgs();
// Modificar argumentos si es necesario if (args.length > 0 && args[0] instanceof String) { args[0] = ((String) args[0]).toUpperCase(); }
// Ejecutar con argumentos modificados Object resultado = pjp.proceed(args);
// Modificar resultado si es necesario if (resultado instanceof String) { return ((String) resultado).toLowerCase(); }
return resultado;}📊 12.7 Casos de Uso Reales
Section titled “📊 12.7 Casos de Uso Reales”Logging de métodos
Section titled “Logging de métodos”@Aspect@Component@Slf4jpublic class LoggingAspect {
@Around("@within(org.springframework.stereotype.Service)") public Object logMetodosService(ProceedingJoinPoint pjp) throws Throwable { String clase = pjp.getTarget().getClass().getSimpleName(); String metodo = pjp.getSignature().getName();
log.debug("→ {}.{}() args={}", clase, metodo, Arrays.toString(pjp.getArgs()));
long inicio = System.currentTimeMillis(); try { Object resultado = pjp.proceed(); long tiempo = System.currentTimeMillis() - inicio;
log.debug("← {}.{}() [{}ms] return={}", clase, metodo, tiempo, resultado); return resultado;
} catch (Exception e) { log.error("✗ {}.{}() error={}", clase, metodo, e.getMessage()); throw e; } }}Medición de rendimiento
Section titled “Medición de rendimiento”@Aspect@Componentpublic class PerformanceAspect {
private final MeterRegistry meterRegistry;
@Around("@annotation(com.miapp.annotation.Timed)") public Object medirTiempo(ProceedingJoinPoint pjp) throws Throwable { String metodo = pjp.getSignature().toShortString();
Timer.Sample sample = Timer.start(meterRegistry);
try { return pjp.proceed(); } finally { sample.stop(Timer.builder("method.execution") .tag("method", metodo) .register(meterRegistry)); } }}Auditoría
Section titled “Auditoría”@Aspect@Componentpublic class AuditAspect {
private final AuditoriaRepository auditoriaRepository;
@AfterReturning( pointcut = "@annotation(auditable)", returning = "resultado" ) public void auditar(JoinPoint jp, Auditable auditable, Object resultado) { Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Auditoria auditoria = Auditoria.builder() .usuario(auth != null ? auth.getName() : "anonymous") .accion(auditable.value()) .metodo(jp.getSignature().toShortString()) .parametros(Arrays.toString(jp.getArgs())) .resultado(String.valueOf(resultado)) .timestamp(LocalDateTime.now()) .build();
auditoriaRepository.save(auditoria); }}
// Uso@Servicepublic class UsuarioService {
@Auditable("CREAR_USUARIO") public Usuario crear(UsuarioDTO dto) { return repository.save(new Usuario(dto)); }}Retry automático
Section titled “Retry automático”@Aspect@Componentpublic class RetryAspect {
@Around("@annotation(retry)") public Object reintentar(ProceedingJoinPoint pjp, Retry retry) throws Throwable { int intentos = 0; Exception ultimaExcepcion = null;
while (intentos < retry.maxAttempts()) { try { return pjp.proceed(); } catch (Exception e) { ultimaExcepcion = e; intentos++;
if (intentos < retry.maxAttempts()) { log.warn("Reintento {}/{} para {}", intentos, retry.maxAttempts(), pjp.getSignature().getName()); Thread.sleep(retry.delay()); } } }
throw ultimaExcepcion; }}
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Retry { int maxAttempts() default 3; long delay() default 1000;}
// Uso@Retry(maxAttempts = 3, delay = 2000)public void llamarServicioExterno() { // Si falla, reintenta hasta 3 veces}⚠️ 12.8 Limitaciones y Buenas Prácticas
Section titled “⚠️ 12.8 Limitaciones y Buenas Prácticas”Limitaciones
Section titled “Limitaciones”// ❌ AOP NO funciona en llamadas internas@Servicepublic class MiServicio {
@Auditable public void metodoA() { // El aspecto NO se aplica aquí metodoB(); // Llamada interna, sin proxy }
@Auditable public void metodoB() { // ... }}
// ✅ Solución: Inyectar el servicio@Servicepublic class MiServicio {
@Autowired private MiServicio self; // Auto-inyección
public void metodoA() { self.metodoB(); // Pasa por el proxy }}Buenas prácticas
Section titled “Buenas prácticas”// ✅ Pointcuts específicos y reutilizables@Aspect@Componentpublic class MiAspect {
// Definir pointcuts nombrados @Pointcut("@within(org.springframework.stereotype.Service)") public void serviceBeans() {}
@Pointcut("execution(* com.miapp.repository.*.*(..))") public void repositoryMethods() {}
// Usar pointcuts nombrados @Before("serviceBeans()") public void antesDeService(JoinPoint jp) {}}
// ✅ Orden de aspectos@Aspect@Order(1) // Se ejecuta primeropublic class SecurityAspect {}
@Aspect@Order(2) // Se ejecuta segundopublic class LoggingAspect {}
🐝