JavaRush /Курси /Модуль 5. Spring /Рефакторимо код за допомогою AOP

Рефакторимо код за допомогою AOP

Модуль 5. Spring
Рівень 3 , Лекція 9
Відкрита

Тепер, коли ми знаємо теорію і вміємо застосовувати AOP на практиці, давайте зробимо наш код кращим.

"Зламати все і переписати заново" — не наш шлях! Рефакторинг — це акуратні покращення коду, які не змінюють його поведінку для користувача. І AOP тут дуже до речі.

Як зрозуміти, що час використовувати AOP? Подивіться на свій код. Бачите однакові шматки для логування в різних місцях? Багато перевірок прав доступу? Схожа обробка помилок? Це вірні ознаки, що настав час винести повторюваний код в аспекти.

Аналіз проблем у коді

Припустимо, у вас є сервіс, який відповідає за управління користувачами. Ось його приклад:


@Service
public class UserService {

    private final UserRepository userRepository;

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

    public User createUser(User user) {
        System.out.println("Починається створення користувача: " + user.getName());
        try {
            User savedUser = userRepository.save(user);
            System.out.println("Користувач успішно створений: " + savedUser.getName());
            return savedUser;
        } catch (Exception e) {
            System.err.println("Помилка при створенні користувача: " + e.getMessage());
            throw e;
        }
    }

    public User getUser(int id) {
        System.out.println("Запит користувача з ID: " + id);
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Користувач з таким ID не знайдено"));
    }
}

Як бачите, тут змішана бізнес-логіка (створення та отримання користувачів) і кросс-секційні задачі (логування). Крім того, обробка виключень дублюється між методами. Виглядає не дуже красиво й важко піддається підтримці.


Виділення логування в аспект

Першим кроком рефакторингу буде винесення логування в аспект. Ми створимо аспект, який буде логувати початок і завершення виконання методів, а також обробляти виключення.

Крок 1: Створення логуючого аспекту


@Aspect
@Component
public class LoggingAspect {

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

    @Around("execution(* com.example.service.*.*(..))")  // Застосовуємо до всіх методів у пакеті service
    public Object logAroundMethods(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        logger.info("Початок виконання методу: {}", methodName);

        Object result;

        try {
            result = joinPoint.proceed(); // Виконуємо цільовий метод
            logger.info("Успішне виконання методу: {}", methodName);
        } catch (Exception e) {
            logger.error("Помилка під час виконання методу: {}", methodName, e);
            throw e; // Обов'язково пробросити виключення далі!
        }

        return result;
    }
}

За допомогою анотації @Around ми створюємо аспекти, які обгортають виконання цільового методу. Тепер логіка логування повністю винесена з бізнес-логіки, і код став значно чистішим.


Оновлення сервісу після рефакторингу

Тепер наш UserService виглядає так:


@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("Користувач з таким ID не знайдено"));
    }
}

Як бачите, логіка стала компактнішою і чистішою. Логування тепер знаходиться в аспекті, і ми можемо використовувати його у всіх сервісах без зміни їхнього коду.


Оптимізація обробки виключень

Наступним кроком буде обробка виключень. Зараз виключення обробляються в аспекту логування, але часто розробники хочуть мати можливість централізовано керувати виключеннями.

Крок 2: Створення аспекту для обробки помилок


@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) {
        // Логуюмо помилку
        logger.error("Сталася помилка: {}", ex.getMessage(), ex);
        // Тут можна додати додаткову обробку, наприклад, повідомлення команди
    }
}

Цей аспект спрацьовуватиме після викидання виключень у сервісах. Він подбає про логування помилок, не засмічуючи основний код.


Розширені можливості: перевірка безпеки

Припустимо, ми хочемо обмежити доступ до методів залежно від ролі користувача. Наприклад, лише адміністратор може створювати користувачів.

Крок 3: Створення аспекту для перевірки безпеки


@Aspect
@Component
public class SecurityAspect {

    @Before("execution(* com.example.service.UserService.createUser(..))")
    public void checkCreateUserAccess() {
        // Перевіряємо, чи має поточний користувач право створювати користувачів
        // Код нижче — лише приклад!
        boolean hasAccess = SecurityContextHolder.getContext().getAuthentication().getAuthorities()
                .contains(new SimpleGrantedAuthority("ROLE_ADMIN"));

        if (!hasAccess) {
            throw new SecurityException("У поточного користувача немає прав для виконання цієї дії");
        }
    }
}

Тепер доступ до методу createUser буде перевірятися автоматично перед його викликом.


Тестування змін

Після рефакторингу важливо переконатися, що ми нічого не зламали. Додаємо тести, щоб перевірити:

  1. Логування працює і містить коректні записи.
  2. Виключення обробляються коректно, і додаток не падає.
  3. Перевірка безпеки правильно обмежує доступ.

Корисні поради та розповсюджені помилки

  • Зайві аспекти: не створюйте аспекти для кожної дрібниці, інакше AOP стане надмірним і ускладнить проєкт.
  • Продуктивність: AOP може трохи уповільнити додаток, якщо використовувати його для методів, що викликаються дуже часто. Наприклад, уникайте логування в аспектах для методів, які виконуються в циклах з високою частотою.
  • Помилки в pointcut-виразах: невірно налаштовані pointcut-вирази можуть випадково перехоплювати зайві методи або, навпаки, ігнорувати важливі методи. Ретельно перевіряйте ваші вирази.
  • Плутанина з проксі: не забувайте, що аспекти працюють через проксі. Якщо ви викличете метод з того ж класу, де він визначений, аспект може не спрацювати.

Досягнення після рефакторингу

Після рефакторингу з використанням AOP код став:

  1. Чистим: логіка сервісу тепер не містить повторюваного коду для логування, безпеки чи обробки виключень.
  2. Модульним: кросс-секційні задачі винесені в аспекти, що полегшує їх підтримку.
  3. Легко змінюваним: ми можемо додати або змінити поведінку, наприклад, логування чи безпеку, змінюючи лише аспекти, а не кожен метод у сервісах.

Ось так виглядає магія AOP! Це як чарівний шар між вашою бізнес-логікою і "усім іншим". Наступного разу, коли хтось скаже, що його код "ідеальний без аспектів", покажіть йому нашу рефакторизацію. 😉

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ