JavaRush /Курси /Модуль 5. Spring /Конфігурація Pointcut і створення кастомних аспектів

Конфігурація Pointcut і створення кастомних аспектів

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

Pointcut — це вираз, який відповідає на питання "де саме в коді має спрацювати аспект?". По суті, це фільтр, який вибирає потрібні методи з усього додатку. Наприклад, "усі методи в сервісному шарі" або "методи з певною анотацією".

В Spring AOP для опису pointcut-ів використовується синтаксис AspectJ. Так, спочатку він може виглядати як код з RSA-шифруванням, але насправді все досить логічно. Давайте розберемо.

Вбудовані вирази Pointcut

Ось найпоширеніші вирази, які вам знадобляться:

Вираз Опис
execution Підходить для методів. Використовується в 99% випадків
within Вказує область (наприклад, конкретний клас або пакет)
args Підходить для методів з певними аргументами
this і target Вказує на об'єкт проксі (this) або реальний цільовий об'єкт (target)
bean Підходить для бінів з певною назвою

Приклад простого Pointcut-виразу:


@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayerMethods() {
    // Цей метод просто маркер для Pointcut
}

Давайте розберемо це "заклинання":

  • execution — ми націлюємося на методи.
  • * — будь-який тип повернення (void, String, int, etc).
  • com.example.service.*.* — будь-який клас у пакеті com.example.service та будь-який метод.
  • (..) — будь-який набір параметрів.

Трохи про Wildcards

Wildcards (підстановчі символи) у виразах Pointcut дозволяють нам бути гнучкими. Ось коротке керівництво:

  • * — означає "будь-який". Приклад: execution(* com.example..*Controller.*(..)) — будь-який метод в будь-якому класі, що містить Controller в імені.
  • .. — означає "будь-який рівень вкладеності". Наприклад, com.example..service включає всі підпакети від com.example.

Приклад: заглиблюємося в Pointcut

Давайте спробуємо щось трохи складніше. Уявімо, що ми хочемо додати логування для всіх методів класу, які починаються з save.


@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example..*Service.save*(..))")
    public void saveMethodsPointcut() {
        // Pointcut для методів, які починаються з save
    }

    @Before("saveMethodsPointcut()")
    public void beforeSaveAdvice(JoinPoint joinPoint) {
        System.out.println("Виклик методу: " + joinPoint.getSignature().getName());
    }
}

Тут ми:

  • визначаємо точку з'єднання для всіх методів, що починаються з save в будь-якому класі з підпакетів com.example, назви яких закінчуються на Service.
  • Перед викликом таких методів ми логруємо ім'я методу.

Композиція Pointcut-виразів

Ми вже згадували, що іноді потрібно об'єднати кілька Pointcut-ів. Наприклад, ми хочемо впливати тільки на методи з певного пакету, і щоб вони ще й починалися з певного слова.

В AspectJ є логічні оператори для композицій:

  • && — логічне "І".
  • || — логічне "АБО".
  • ! — логічне "НЕ".

Ось приклад:


@Pointcut("within(com.example..*)")
public void withinExamplePackage() {}

@Pointcut("execution(* save*(..))")
public void saveMethods() {}

@Pointcut("withinExamplePackage() && saveMethods()")
public void saveMethodsInExamplePackage() {}

Таким чином, saveMethodsInExamplePackage() спрацює тільки на методах, які починаються з save, і лежать у пакетах com.example.


Створення кастомного аспекту

Тепер застосуємо отримані знання на практиці. Припустимо, у нашому додатку є сервіс, який відповідає за управління користувачами (UserService). Ми хочемо:

  1. Логувати виклики методів, що починаються з find.
  2. Викликати додаткову логіку лише тоді, коли метод повертає String.

Реалізація


@Aspect
@Component
public class CustomAspect {

    // Логування всіх методів, які починаються з find
    @Pointcut("execution(* com.example.service.UserService.find*(..))")
    public void findMethods() {}

    // Виконання дії перед викликом методу
    @Before("findMethods()")
    public void logBeforeFind(JoinPoint joinPoint) {
        System.out.println("Виклик методу: " + joinPoint.getSignature().getName());
    }

    // Реакція на успішне виконання методу, який повертає String
    @AfterReturning(
        pointcut = "findMethods()",
        returning = "result"
    )
    public void afterReturningFind(JoinPoint joinPoint, Object result) {
        if (result instanceof String) {
            System.out.println("Метод " + joinPoint.getSignature().getName() +
                               " успішно повернув значення: " + result);
        }
    }
}

Що відбувається:

  1. За допомогою @Pointcut ми вибираємо тільки методи в класі UserService, імена яких починаються з find.
  2. Використовуємо @Before для логування виклику.
  3. Використовуємо @AfterReturning, щоб додатково обробити повернуте значення, якщо воно є рядком.

Практика: створення складного кастомного аспекту

Спробуємо ускладнити завдання. Припустимо, нам потрібно логувати всі методи контролерів, які анотовані @GetMapping.


@Aspect
@Component
public class ControllerLoggingAspect {

    // Pointcut для методів з анотацією @GetMapping
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void getMappingMethods() {}

    // Логування до виконання методу
    @Before("getMappingMethods()")
    public void logBeforeGetMapping(JoinPoint joinPoint) {
        System.out.println("Виклик методу контролера: " + joinPoint.getSignature().getName());
    }
}

Ми використовуємо анотацію @annotation, щоб чітко визначити методи, на які націлюємося (у даному випадку методи з @GetMapping). Бачиш, наскільки потужним стає твій інструмент?


Налагодження й типові помилки

AOP — не позбавлений своїх загадок. Ось кілька речей, про які варто пам'ятати:

  1. Якщо твій аспект не працює, переконайся, що клас аспекту позначений анотацією @Component і зареєстрований у контексті Spring.
  2. Перевір, що ти правильно вказуєш шлях пакетів у execution. Пропущений рівень вкладеності або опечатка можуть звести всі зусилля нанівець.
  3. Зверни увагу на проксі: якщо ти викликаєш метод класу всередині того ж самого класу, аспект може не спрацювати.

Ручна перевірка Pointcut-виразів

Якщо цікаво, які саме методи потрапляють під твій Pointcut-вираз, ти завжди можеш це перевірити вручну:


@Before("execution(* com.example.service.*.*(..))")
public void debugPointcut(JoinPoint joinPoint) {
    System.out.println("Потрапив у Pointcut: " + joinPoint.getSignature().getName());
}

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


Ти готовий створювати аспекти, які зможуть керувати бізнес-логікою твого додатка! На наступному етапі ми заглибимось ще більше в проксі-об'єкти і подивимося на їх приховані секрети.

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