CodeGym /Cursos /Módulo 5. Spring /Refactorizamos el código con AOP

Refactorizamos el código con AOP

Módulo 5. Spring
Nivel 3 , Lección 9
Disponible

Ahora que conocemos la teoría y sabemos aplicar AOP en la práctica, vamos a mejorar nuestro código.

"Romperlo todo y reescribir desde cero" — ¡no es nuestro estilo! El refactorizado son mejoras cuidadosas al código que no cambian su comportamiento para el usuario. Y AOP viene al rescate.

¿Cómo saber cuándo usar AOP? Mira tu código. ¿Ves trozos idénticos para registrar eventos en varios sitios? ¿Muchas comprobaciones de permisos? ¿Manejo de errores similar? Esos son buenos indicadores de que toca mover el código repetido a aspectos.

Análisis de problemas en el código

Supongamos que tienes un servicio que gestiona usuarios. Aquí un ejemplo:


@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        System.out.println("Comienza la creación del usuario: " + user.getName());
        try {
            User savedUser = userRepository.save(user);
            System.out.println("Usuario creado con éxito: " + savedUser.getName());
            return savedUser;
        } catch (Exception e) {
            System.err.println("Error al crear el usuario: " + e.getMessage());
            throw e;
        }
    }

    public User getUser(int id) {
        System.out.println("Consulta del usuario con ID: " + id);
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("No se encontró un usuario con ese ID"));
    }
}

Como ves, aquí está mezclada la lógica de negocio (crear y obtener usuarios) con tareas cross-cutting (registro). Además, el manejo de excepciones se duplica entre métodos. No queda muy limpio y es difícil de mantener.


Extraer el registro en un aspecto

El primer paso del refactorizado será sacar el registro a un aspecto. Crearemos un aspecto que registre el inicio y el fin de la ejecución de métodos, y además maneje excepciones.

Paso 1: Crear el aspecto de logging


@Aspect
@Component
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("execution(* com.example.service.*.*(..))")  // Se aplica a todos los métodos del paquete service
    public Object logAroundMethods(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Inicio de la ejecución del método: {}", methodName);

        Object result;

        try {
            result = joinPoint.proceed(); // Ejecutamos el método objetivo
            logger.info("Ejecución exitosa del método: {}", methodName);
        } catch (Exception e) {
            logger.error("Error al ejecutar el método: {}", methodName, e);
            throw e; // ¡Hay que re-lanzar la excepción!
        }

        return result;
    }
}

Con la anotación @Around creamos aspectos que envuelven la ejecución del método objetivo. Ahora la lógica de registro está totalmente separada de la lógica de negocio, y el código queda mucho más limpio.


Actualizar el servicio tras el refactorizado

Ahora nuestro UserService se ve así:


@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser(User user) {
        return userRepository.save(user);
    }

    public User getUser(int id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("No se encontró un usuario con ese ID"));
    }
}

Como puedes ver, la lógica quedó más compacta y limpia. El registro ahora vive en el aspecto, y podemos usarlo en todos los servicios sin tocar su código.


Optimización del manejo de excepciones

El siguiente paso será el manejo centralizado de excepciones. Ahora los errores se registran en el aspecto de logging, pero a menudo los desarrolladores quieren gestionar las excepciones de forma centralizada.

Paso 2: Crear un aspecto para el manejo de errores


@Aspect
@Component
public class ErrorHandlingAspect {

    private static final Logger logger = LoggerFactory.getLogger(ErrorHandlingAspect.class);

    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void handleException(Exception ex) {
        // Registramos el error
        logger.error("Ocurrió una excepción: {}", ex.getMessage(), ex);
        // Aquí se puede añadir manejo adicional, por ejemplo, notificar al equipo
    }
}

Este aspecto se disparará después de que se lancen excepciones en los servicios. Se encargará de registrar errores sin ensuciar el código principal.


Posibilidades avanzadas: comprobación de seguridad

Supongamos que queremos restringir el acceso a métodos según el rol del usuario. Por ejemplo, solo el administrador puede crear usuarios.

Paso 3: Crear un aspecto para la comprobación de seguridad


@Aspect
@Component
public class SecurityAspect {

    @Before("execution(* com.example.service.UserService.createUser(..))")
    public void checkCreateUserAccess() {
        // Comprobamos si el usuario actual tiene permiso para crear usuarios
        // El código de abajo es solo un ejemplo!
        boolean hasAccess = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
                .contains(new SimpleGrantedAuthority("ROLE_ADMIN"));

        if (!hasAccess) {
            throw new SecurityException("El usuario actual no tiene permisos para realizar esta acción");
        }
    }
}

Ahora el acceso al método createUser se comprobará automáticamente antes de su ejecución.


Probar los cambios

Tras el refactorizado es importante asegurarse de que no rompimos nada. Añadimos tests para comprobar:

  1. Que el registro funciona y contiene entradas correctas.
  2. Que las excepciones se manejan correctamente y la aplicación no se cae.
  3. Que la comprobación de seguridad limita el acceso adecuadamente.

Consejos útiles y errores comunes

  • Aspectos excesivos: no crees un aspecto para cada tontería, o AOP se volverá excesivo y complicará el proyecto.
  • Rendimiento: AOP puede ralentizar un poco la app si lo usas en métodos que se ejecutan con mucha frecuencia. Por ejemplo, evita registrar desde aspectos métodos que se llaman dentro de bucles muy intensos.
  • Errores en las expresiones pointcut: pointcut mal configurados pueden interceptar métodos de más o, al contrario, ignorar métodos importantes. Revisa bien tus expresiones.
  • Confusión con proxies: recuerda que los aspectos funcionan vía proxies. Si llamas a un método desde la misma clase donde está definido, el aspecto puede no aplicarse.

Logros tras el refactorizado

Después de refactorizar con AOP, el código quedó:

  1. Limpio: la lógica del servicio ya no contiene código repetido para registro, seguridad o manejo de excepciones.
  2. Modular: las tareas cross-cutting se movieron a aspectos, lo que facilita su mantenimiento.
  3. Fácilmente modificable: podemos añadir o cambiar comportamientos, por ejemplo, logging o seguridad, modificando solo los aspectos y no cada método de los servicios.

Así es la magia de AOP: es como una capa mágica entre tu lógica de negocio y "todo lo demás". La próxima vez que alguien diga que su código "es perfecto sin aspectos", muéstrale nuestra refactorización. 😉

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION