🍃 Розділ 5 · Питання #16

Що таке аспект, advice, pointcut, join point

У парадигмі аспектно-орієнтованого програмування (AOP) ці 4 поняття утворюють базовий термінологічний каркас.


🟢 Junior Level

У парадигмі аспектно-орієнтованого програмування (AOP) ці 4 поняття утворюють базовий термінологічний каркас.

Коротка таблиця термінів

Термін Роль в AOP Просте пояснення Приклад із життя
Join Point Де можливо Будь-яка потенційна точка в програмі, куди можна вбудувати логіку. У Spring — це виклик будь-якого методу біна. Вхідні двері до будівлі.
Pointcut Де саме Фільтр (вираз-предикат), що відбирає конкретні Join Points. Правило: «Перевіряти перепустку лише на дверях із табличкою “Склад”».
Advice Що робити Код наскрізної логіки, що виконується до, після або замість цільового методу. Дія охоронця: «Записати час прибуття в журнал».
Aspect Модуль Java-клас (@Aspect), який об’єднує правила (Pointcut) та дії (Advice). Посадова інструкція служби безпеки.

Базовий приклад

package com.example.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

// 1. ASPECT: модуль наскрізної функціональності
@Aspect
@Component
public class MetricsAspect {

    // 2. POINTCUT: фільтр (обираємо всі методи сервісів з анотацією @Monitored)
    @Pointcut("@annotation(com.example.annotation.Monitored)")
    public void monitoredMethods() { }

    // 3. ADVICE: дія навколо виклику (Around)
    // 4. JOIN POINT: конкретний виконуваний метод (передається як ProceedingJoinPoint)
    @Around("monitoredMethods()")
    public Object trackExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            // Виклик оригінального методу (Join Point)
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.println("Метод " + joinPoint.getSignature().getName() 
                    + " завершився за " + duration + " ms");
        }
    }
}

🟡 Middle Level

5 типів Advice (Порад) та порядок їх виконання

Spring AOP підтримує 5 типів порад:

[Виклик методу через Proxy]
            │
            ▼
    1. @Before
            │
            ▼
    ┌──────────────────────┐
    │  Target Method Call  │ ── (викинув Exception) ──► 4. @AfterThrowing
    └──────────────────────┘
            │ (успішне завершення)
            ▼
    2. @AfterReturning
            │
            ▼
    3. @After (аналог блоку finally, виконується ЗАВЖДИ)
package com.example.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DetailedLoggingAspect {

    // 1. До виклику методу
    @Before("execution(* com.example.service.UserService.*(..))")
    public void beforeMethod(JoinPoint jp) {
        System.out.println("Before: виклик " + jp.getSignature().getName());
    }

    // 2. Після успішного завершення (є доступ до returnedValue)
    @AfterReturning(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        returning = "result"
    )
    public void afterSuccessfulReturn(JoinPoint jp, Object result) {
        System.out.println("AfterReturning: метод повернув " + result);
    }

    // 3. Лише якщо метод викинув виняток
    @AfterThrowing(
        pointcut = "execution(* com.example.service.UserService.*(..))",
        throwing = "ex"
    )
    public void afterFailedExecution(JoinPoint jp, Throwable ex) {
        System.out.println("AfterThrowing: помилка " + ex.getMessage());
    }

    // 4. Виконується ЗАВЖДИ (аналог блоку finally)
    @After("execution(* com.example.service.UserService.*(..))")
    public void afterFinally(JoinPoint jp) {
        System.out.println("After (finally): очищення ресурсів");
    }
}

Синтаксис Pointcut Designators (PCD)

Spring AOP підтримує такі ключові предикати:

  1. execution (найчастіший): зіставлення за сигнатурою методу. execution(modifiers-pattern? ret-type declaring-type?name(param) throws?)
    // Будь-який public-метод у пакеті service, що повертає будь-яке значення
    @Pointcut("execution(public * com.example.service..*.*(..))")
    
  2. within: обмежує область типами всередині визначених класів або пакетів.
    @Pointcut("within(com.example.service..*)")
    
  3. @annotation: зіставлення за анотацією над методом.
    @Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
    
  4. @target / @within: зіставлення за анотацією над класом.
    @Pointcut("@target(org.springframework.stereotype.Service)")
    
  5. bean (специфічний для Spring): вибір біна за ім’ям або шаблоном імені.
    @Pointcut("bean(*Service) || bean(orderRepository)")
    

🔴 Senior Level

Анатомія JoinPoint: різниця між getTarget() та getThis()

Усередині Advice розробник отримує доступ до контексту виклику через інтерфейс JoinPoint:

@Around("execution(* com.example.service..*(..))")
public Object inspect(ProceedingJoinPoint pjp) throws Throwable {
    Object target = pjp.getTarget(); // Вихідний бін (Target Object без проксі)
    Object proxy  = pjp.getThis();   // Сам згенерований PROXY-об'єкт!

    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Method method = signature.getMethod();
    Object[] args = pjp.getArgs(); // Увага: створює копію масиву параметрів!

    return pjp.proceed();
}

Практичний нюанс: pjp.getTarget() повертає реальний бізнес-об’єкт (клас UserService), а pjp.getThis() повертає UserService$$SpringCGLIB$$0. Якщо ви бажаєте викликати інший метод того самого біна через проксі (щоб спрацював транзакційний аспект), виклик необхідно здійснювати на pjp.getThis(), а не на this!

Статичні vs Динамічні Pointcut

З погляду продуктивності середовища виконання Pointcuts поділяються на дві категорії:

  1. Статичні (Static Pointcuts):
    • Вирази типу execution, within, @annotation, @within.
    • Перевіряються контейнером рівно один раз під час старту застосунку (на фазі BeanPostProcessor).
    • Результат кешується: контейнер заздалегідь знає, чи обгортати бін у проксі та які перехоплювачі призначати на метод. У рантаймі накладні витрати перевірки дорівнюють нулю.
  2. Динамічні (Dynamic Pointcuts):
    • Вирази типу args(com.example.SuperUser, ..) або перевірка значень аргументів у рантаймі (MethodMatcher.isRuntime() == true).
    • Контейнер змушений перевіряти типи та значення реальних аргументів під час кожного виклику методу.
    • Створює постійне навантаження на CPU та блокує JIT-оптимізації (method inlining).

Ідіоматична прив’язка аргументів та анотацій у Pointcut

Замість того щоби витягувати параметри вручну через рефлексію pjp.getArgs(), AspectJ дозволяє прозоро інжектувати їх прямо в параметри методу Advice:

package com.example.aspect;

import com.example.annotation.Auditable;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SmartAuditAspect {

    // Анотація @Auditable та перший аргумент Long userId автоматично прив'язуються!
    @Before("@annotation(auditable) && args(userId, ..)")
    public void auditAction(JoinPoint jp, Auditable auditable, Long userId) {
        String actionName = auditable.action();
        System.out.println("Користувач " + userId + " виконує дію: " + actionName);
    }
}

Переваги: код типобезпечний, не вимагає небезпечних приведень типів (Cast) і працює швидше за ручну рефлексію.


🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

  • Join Point (Точка з’єднання): будь-яке доступне місце в програмі для впровадження аспекту. У Spring AOP це лише виклики public-методів Spring-бінів.
  • Pointcut (Зріз): предикатний вираз (AspectJ Pointcut Expression), що фільтрує методи (наприклад, execution(* com.service..*(..)) або @annotation(Transactional)).
  • Advice (Порада): код, що виконується в точці зрізу (@Before, @AfterReturning, @AfterThrowing, @After, @Around).
  • Aspect (Аспект): модуль (Spring-бін з анотацією @Aspect), що об’єднує Pointcut та Advice.

4 каверзних питання з відповідями

1. У чому різниця між joinPoint.getTarget() та joinPoint.getThis()?

Відповідь: getTarget() повертає посилання на оригінальний цільовий об’єкт (сирий бін без проксі-обгорток). getThis() повертає посилання на сам згенерований AOP-проксі. Якщо потрібно виконати само-виклик іншого методу того самого класу зі збереженням поведінки аспектів, його слід адресувати об’єкту getThis().

2. У якому порядку викликаються @After, @AfterReturning та @AfterThrowing при успішному завершенні та при помилці?

Відповідь:

  • При успішному завершенні: спочатку відпрацьовує метод, потім @AfterReturning, потім @After (finally).
  • При викиданні винятку: спочатку виникає помилка, потім викликається @AfterThrowing, потім @After (finally).

3. Чому статичні Pointcut працюють швидше за динамічні?

Відповідь: Статичні вирази (execution, @annotation) обчислюються одноразово під час створення біна в BeanPostProcessor і кешуються. Динамічні вирази (наприклад, інспекція значень аргументів у рантаймі) вимагають перевірки умов інтерцептором під час кожного виклику методу.

4. Що поверне joinPoint.getArgs() і який оверхед це несе?

Відповідь: Метод повертає масив Object[], що містить параметри виклику. AspectJ створює нову копію масиву під час кожного виклику getArgs(), до того ж примітивні типи пакуються в об’єкти-обгортки (autoboxing). У високонавантажених методах це створює додатковий тиск на збирач сміття (GC allocation rate).


Червоні прапорці (чого категорично не можна говорити)

  • ❌ «Join Point у Spring AOP може бути полем або приватним методом» — у Spring AOP Join Point — це суворо public-методи бінів.
  • ❌ «@After викликається лише при успішному завершенні методу» — @After виконується завжди, подібно до блоку finally. Лише при успіху викликається @AfterReturning.
  • ❌ «Pointcut містить бізнес-логіку виконання» — Pointcut містить виключно предикат (фільтр). Логіку містить Advice.
  • ❌ «getTarget() та getThis() повертають один і той самий об’єкт» — getTarget() повертає реальний об’єкт, getThis() повертає проксі.

Пов’язані теми