Тепер, коли ми знаємо теорію і вміємо застосовувати 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 буде перевірятися автоматично перед його викликом.
Тестування змін
Після рефакторингу важливо переконатися, що ми нічого не зламали. Додаємо тести, щоб перевірити:
- Логування працює і містить коректні записи.
- Виключення обробляються коректно, і додаток не падає.
- Перевірка безпеки правильно обмежує доступ.
Корисні поради та розповсюджені помилки
- Зайві аспекти: не створюйте аспекти для кожної дрібниці, інакше AOP стане надмірним і ускладнить проєкт.
- Продуктивність: AOP може трохи уповільнити додаток, якщо використовувати його для методів, що викликаються дуже часто. Наприклад, уникайте логування в аспектах для методів, які виконуються в циклах з високою частотою.
- Помилки в pointcut-виразах: невірно налаштовані pointcut-вирази можуть випадково перехоплювати зайві методи або, навпаки, ігнорувати важливі методи. Ретельно перевіряйте ваші вирази.
- Плутанина з проксі: не забувайте, що аспекти працюють через проксі. Якщо ви викличете метод з того ж класу, де він визначений, аспект може не спрацювати.
Досягнення після рефакторингу
Після рефакторингу з використанням AOP код став:
- Чистим: логіка сервісу тепер не містить повторюваного коду для логування, безпеки чи обробки виключень.
- Модульним: кросс-секційні задачі винесені в аспекти, що полегшує їх підтримку.
- Легко змінюваним: ми можемо додати або змінити поведінку, наприклад, логування чи безпеку, змінюючи лише аспекти, а не кожен метод у сервісах.
Ось так виглядає магія AOP! Це як чарівний шар між вашою бізнес-логікою і "усім іншим". Наступного разу, коли хтось скаже, що його код "ідеальний без аспектів", покажіть йому нашу рефакторизацію. 😉
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ