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! Это как волшебный слой между вашей бизнес-логикой и "всем остальным". В следующий раз, когда кто-то скажет, что его код "идеален без аспектов", покажите ему нашу рефакторизацию. 😉

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ