Що таке аспект, 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 підтримує такі ключові предикати:
execution(найчастіший): зіставлення за сигнатурою методу.execution(modifiers-pattern? ret-type declaring-type?name(param) throws?)// Будь-який public-метод у пакеті service, що повертає будь-яке значення @Pointcut("execution(public * com.example.service..*.*(..))")within: обмежує область типами всередині визначених класів або пакетів.@Pointcut("within(com.example.service..*)")@annotation: зіставлення за анотацією над методом.@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")@target/@within: зіставлення за анотацією над класом.@Pointcut("@target(org.springframework.stereotype.Service)")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 поділяються на дві категорії:
- Статичні (Static Pointcuts):
- Вирази типу
execution,within,@annotation,@within. - Перевіряються контейнером рівно один раз під час старту застосунку (на фазі
BeanPostProcessor). - Результат кешується: контейнер заздалегідь знає, чи обгортати бін у проксі та які перехоплювачі призначати на метод. У рантаймі накладні витрати перевірки дорівнюють нулю.
- Вирази типу
- Динамічні (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()повертає проксі.