Что такое аспект, 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-оптимизации.
- Выражения типа
Идиоматичная привязка аргументов и аннотаций в 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 каверзных вопроса с ответами
-
В чём разница между
joinPoint.getTarget()иjoinPoint.getThis()? Ответ:getTarget()возвращает ссылку на оригинальный целевой объект (сырой бин без оберток).getThis()возвращает ссылку на сам сгенерированный AOP-прокси. Если требуется выполнить само-вызов другого метода того же класса с сохранением аспектов, его нужно адресовать объектуgetThis(). - В каком порядке вызываются
@After,@AfterReturningи@AfterThrowingпри успешном завершении и при ошибке? Ответ:- При успешном завершении: сначала отрабатывает метод, затем
@AfterReturning, затем@After(finally). - При выбросе исключения: сначала выбрасывается ошибка, затем вызывается
@AfterThrowing, затем@After(finally).
- При успешном завершении: сначала отрабатывает метод, затем
-
Почему статические Pointcut работают быстрее динамических? Ответ: Статические выражения (
execution,@annotation) вычисляются однократно при создании бина вBeanPostProcessorи кэшируются. Динамические выражения (например, инспекция значений аргументов в рантайме) требуют проверки условий интерцептором при каждом вызове метода. - Что вернет
joinPoint.getArgs()и какой оверхед это несет? Ответ: Метод возвращает массивObject[], содержащий параметры вызова. AspectJ создает новую копию массива при каждом вызовеgetArgs(), плюс примитивные типы упаковываются в объекты-обертки (boxing). В высоконагруженных методах это генерирует дополнительное давление на Garbage Collector (GC allocation rate).
Красные флаги (чего категорически нельзя говорить)
- ❌ «Join Point в Spring AOP может быть полем или приватным методом» — в Spring AOP Join Point — это строго public-методы бинов.
- ❌ «
@Afterвызывается только при успешном завершении метода» —@Afterвыполняется всегда, как блок finally. Только при успехе вызывается@AfterReturning. - ❌ «Pointcut содержит логику выполнения» — Pointcut содержит только предикат (фильтр). Логику содержит Advice.
- ❌ «
getTarget()иgetThis()возвращают одно и то же» —getTarget()возвращает реальный объект,getThis()возвращает прокси.