Вопрос 16 · Раздел 5

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

4. @Order → порядок выполнения 5. getArgs() → копия массива, осторожно 6. @Around → не забудьте proceed()

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Термины AOP:

Термин Что означает Пример
Join Point Конкретный метод, куда можно вклиниться UserService.save()
Pointcut Правило, ГДЕ применять “Все методы с @Transactional”
Advice ЧТО делать (логика) “Записать время выполнения”
Aspect Модуль, объединяющий Pointcut + Advice Класс с логированием
@Aspect
@Component
public class LoggingAspect {  // ← Aspect
    
    @Around("@annotation(LogTime)")  // ← Pointcut
    public Object logTime(ProceedingJoinPoint pjp) {  // ← Advice
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();  // Join Point вызов
        } finally {
            log.info("Выполнено за " + (System.currentTimeMillis() - start) + "ms");
        }
    }
}

🟡 Middle Level

Types of Advice

@Before("pointcut")
public void before() { }  // До метода

@After("pointcut")
public void after() { }  // После метода (всегда)
// @After = как finally, выполняется ВСЕГДА (и при успехе, и при исключении)
// @AfterReturning = только если метод завершился УСПЕШНО (без исключений)

@AfterReturning(pointcut = "pc", returning = "result")
public void afterReturn(Object result) { }  // После успешного

@AfterThrowing(pointcut = "pc", throwing = "ex")
public void afterThrow(Exception ex) { }  // При исключении

@Around("pointcut")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // До
    Object result = pjp.proceed();  // Вызов метода
    // После
    return result;
}

Static vs Dynamic Pointcuts

Static (быстрый):
  → Проверяется один раз при создании бина
  → По аннотациям, сигнатуре, имени метода
  
Dynamic (медленный):
  → Проверяется при КАЖДОМ вызове
  → По значениям аргументов
  → Оверхед на каждую проверку

🔴 Senior Level

JoinPoint информация

@Around("pointcut")
public Object log(ProceedingJoinPoint pjp) {
    pjp.getSignature();       // Сигнатура метода
    pjp.getArgs();            // Аргументы (копия массива!)
    pjp.getTarget();          // Оригинальный объект
    pjp.getThis();            // Прокси
    
    // getArgs() создаёт копию массива → GC pressure (заметно только при очень частых вызовах аспекта — сотни тысяч в секунду — или при больших массивах аргументов).
}

Aspect = Singleton

// Аспект — Singleton по умолчанию!
// → НЕ храните состояние в полях аспекта!
@Aspect
public class MetricsAspect {
    // ❌ Не потокобезопасно!
    private int callCount = 0;
    
    // ✅ ThreadLocal
    private ThreadLocal<Long> startTime = new ThreadLocal<>();
}

Pipeline

ReflectiveMethodInvocation:
  1. Находит все Advisors
  2. Фильтрует по Pointcut
  3. Сортирует по @Order
  4. Создаёт List<MethodInterceptor>
  5. Рекурсивно проходит по всем
  6. Вызывает target.invoke()

Production Experience

Реальный сценарий: Around без proceed

@Around("@annotation(Cacheable)")
public Object cache(ProceedingJoinPoint pjp) {
    if (cache.has(key)) return cache.get(key);
    // Забыли pjp.proceed()!
    // → Метод НИКОГДА не вызывается!
}

Best Practices

  1. Статические Pointcut → быстрее
  2. Узкие Pointcut → меньше оверхед. Чем точнее pointcut, меньше бинов попадёт под проксирование и меньше проверок при каждом вызове метода.
  3. Без состояния в аспектах (Singleton!)
  4. @Order → порядок выполнения
  5. getArgs() → копия массива, осторожно
  6. @Around → не забудьте proceed()

Резюме для Senior

  • Join Point = контекст вызова
  • Pointcut = фильтр методов
  • Advice = тип внедрения
  • Aspect = логический модуль
  • Pipeline = цепочка интерцепторов
  • Aspect = Singleton → без состояния
  • proceed() → обязательно в @Around

🎯 Шпаргалка для интервью

Обязательно знать:

  • Join Point = конкретный метод, Pointcut = фильтр (где), Advice = логика (что), Aspect = модуль
  • 5 типов Advice: @Before, @After, @AfterReturning, @AfterThrowing, @Around
  • @After выполняется ВСЕГДА (как finally), @AfterReturning — только при успехе
  • Статические Pointcut проверяются один раз при создании бина, динамические — при каждом вызове
  • Аспект = Singleton → НЕ храните состояние в полях, используйте ThreadLocal
  • pjp.proceed() обязательно в @Around — иначе метод не вызовется
  • Pipeline: ReflectiveMethodInvocation фильтрует Advisors по Pointcut, сортирует по @Order, проходит рекурсивно
  • getArgs() создаёт копию массива → GC pressure при частых вызовах

Частые уточняющие вопросы:

  • @After vs @AfterThrowing — когда что использовать? → @After = cleanup ресурсов всегда, @AfterThrowing = обработка ошибок
  • Почему аспект = Singleton? → Spring регистрирует аспекты как обычные бины (по умолчанию Singleton)
  • Статический vs динамический Pointcut — какой выбрать? → Статический быстрее, динамический гибче; предпочитайте статический
  • Что будет, если забыть pjp.proceed()? → Оригинальный метод не выполнится вообще

Красные флаги (НЕ говорить):

  • «@After только при успехе» — это @AfterReturning
  • «Аспект хранит состояние — нормально» — Singleton, не потокобезопасно
  • «Динамический Pointcut быстрее» — наоборот, проверяется каждый раз
  • «getArgs() возвращает оригинальный массив» — создаёт копию

Связанные темы:

  • [[15. Что такое AOP (Aspect-Oriented Programming)]]
  • [[13. Что такое прокси в Spring]]
  • [[17. Что делает аннотация @Transactional]]
  • [[18. Почему @Transactional не работает при self-invocation]]