🍃 Раздел 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-оптимизации.

Идиоматичная привязка аргументов и аннотаций в 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(), плюс примитивные типы упаковываются в объекты-обертки (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() возвращает прокси.

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