← назад к разделу

AOP (Aspect-Oriented Programming) — способ выделить cross-cutting concerns (логирование, метрики, безопасность, транзакции) в отдельные модули и применять к большому количеству методов без копи-паста. В Spring AOP используется внутри: @Transactional, @Async, @Cacheable, @Scheduled, @Retryable, Spring Security — всё это AOP-прокси.

Своими руками @Aspect пишут реже, чем кажется. Чаще достаточно встроенных аннотаций или нативных средств — Filter, HandlerInterceptor, MeterRegistry. Но знать, как AOP работает, нужно — иначе непонятно, почему @Transactional иногда «не срабатывает».

Терминология

  • Aspect — модуль, инкапсулирующий cross-cutting concern (логирование, метрики).
  • Join point — точка в коде, где может срабатывать aspect (для Spring AOP это только вызов method'а).
  • Pointcut — выражение, выбирающее join points.
  • Advice — действие, привязанное к pointcut (@Before, @After, @AfterReturning, @AfterThrowing, @Around).

Пример: простой логгер

@Aspect
@Component
@Slf4j
public class MethodLoggingAspect {

    @Pointcut("execution(* com.example.app.usecase..*.handle(..))")
    public void useCaseHandlerMethods() {}

    @Around("useCaseHandlerMethods()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        var methodName = pjp.getSignature().toShortString();
        var start = System.currentTimeMillis();
        log.info("→ {} args={}", methodName, Arrays.toString(pjp.getArgs()));
        try {
            Object result = pjp.proceed();
            log.info("← {} took={}ms", methodName, System.currentTimeMillis() - start);
            return result;
        } catch (Throwable t) {
            log.error("✗ {} threw {}", methodName, t.getClass().getSimpleName());
            throw t;
        }
    }
}

Применится ко всем методам handle(...) в пакете usecase. Поведение прозрачно для самого Handler'а — он ничего не знает про aspect.

Активируется через @EnableAspectJAutoProxy (по умолчанию включено в Spring Boot).

Виды advice

  • @Before — до вызова метода (нельзя изменить аргументы или результат).
  • @AfterReturning — после успешного возврата (можно увидеть результат).
  • @AfterThrowing — после exception (можно увидеть и сохранить).
  • @After — после возврата OR exception (finally-эквивалент).
  • @Around — оборачивает вызов целиком: можете изменить аргументы, перехватить exception, заменить результат. Самый мощный, но и самый сложный.

В практике для нетривиальных aspect'ов используется @Around, потому что он сочетает все остальные.

Pointcut expressions

@Pointcut("execution(* com.example.repo.*Repository.*(..))")        // все методы всех Repository
@Pointcut("execution(public * com.example.api..*(..))")              // все public-методы в api
@Pointcut("@within(org.springframework.stereotype.Service)")          // все методы @Service
@Pointcut("@annotation(com.example.Timed)")                           // методы, помеченные @Timed
@Pointcut("execution(* save(..)) && args(entity,..)")                 // save(entity, ...) с возможностью получить arg

Pointcut можно комбинировать через &&, ||, !:

@Pointcut("execution(* com.example..*(..)) && !@within(InternalOnly)")

Custom-аннотация + AOP

Типичный паттерн — своя аннотация для маркировки + один aspect для всех:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audited {
    String action();
}

@Aspect
@Component
@RequiredArgsConstructor
public class AuditAspect {

    private final AuditService audit;

    @Around("@annotation(audited)")
    public Object record(ProceedingJoinPoint pjp, Audited audited) throws Throwable {
        var actor = SecurityContextHolder.getContext().getAuthentication().getName();
        try {
            Object result = pjp.proceed();
            audit.log(actor, audited.action(), "ok");
            return result;
        } catch (Throwable t) {
            audit.log(actor, audited.action(), "failed: " + t.getMessage());
            throw t;
        }
    }
}

@Service
public class OrderService {
    @Audited(action = "create-order")
    public OrderId create(CreateOrderCommand cmd) { ... }
}

JDK Proxy vs CGLIB

Spring AOP создаёт прокси вокруг бина. Два механизма:

  • JDK Dynamic Proxy — работает только если класс реализует интерфейс. Прокси наследуется от интерфейса, оборачивает реальный экземпляр.
  • CGLIB — генерирует подкласс целевого класса, переопределяет методы. Работает с любыми классами (без интерфейса).

С Spring Boot 2.0+ по умолчанию используется CGLIB для всего. Раньше Spring переключался на JDK Proxy, если был интерфейс, что приводило к багам с приведением типов.

Следствия для CGLIB:

  • final методы не проксируются — компилятор не позволит переопределить.
  • final классы не проксируются вообще — не получится подкласс.
  • private методы не видны прокси.

Поэтому правило: @Transactional/@Async/@Cacheable — только на public non-final методах non-final классов.

Ограничения Spring AOP

1. Self-invocation (та же ловушка, что у @Transactional)

@Service
public class OrderService {
    public void batch() {
        this.processOne();  // ← через this, прокси не задействован
    }

    @Audited(action = "process-one")
    public void processOne() { ... }
}

Решение: вынести в другой бин или внедрить self-reference через ObjectProvider<OrderService>.

2. Только Spring-managed бины

Aspect не сработает на объектах, созданных через new. Spring инструментирует только бины из контекста.

3. Только method call

Spring AOP не покрывает поля, конструкторы, статические методы. Если нужно AfterThrowing на конструкторе — это AspectJ (compile-time или load-time weaving), не Spring AOP.

4. Порядок aspect'ов

Когда на один метод применяется несколько aspect'ов (например, @Transactional + @Audited + @Timed), порядок управляется через @Order:

@Aspect
@Component
@Order(1)
public class AuditAspect { ... }   // outermost

@Aspect
@Component
@Order(2)
public class TimedAspect { ... }   // middle

// @Transactional имеет Ordered.LOWEST_PRECEDENCE по умолчанию → innermost

Меньше @Order = снаружи. Если порядок важен — задавайте явно.

Когда @Aspect, когда нет

ЗадачаПодход
Логирование вызовов method-уровняМожно AOP, но обычно лучше Filter (HTTP-уровень) или Micrometer Observation
Метрики времени выполненияMicrometer.Timer + custom aspect, или MeterRegistry.timer(...) напрямую
Авторизация на method-уровне@PreAuthorize (Spring Security уже использует AOP)
Кэширование результата@Cacheable
Retry с backoff@Retryable
Транзакция@Transactional
Custom audit-логи на доменных операцияхCustom aspect — хороший fit
Логирование в одном конкретном сервисеПрямой код, без AOP — проще

Правило: если поведение применяется к дисциплинированно помеченным методам (через custom аннотацию) — AOP. Если поведение специфично для одного-двух методов — прямо в коде.

AspectJ — за пределами Spring

Spring AOP проксирует только public method calls в Spring-managed бинах. Для большего:

  • Constructor pointcuts — нужен AspectJ.
  • Field access — нужен AspectJ.
  • Любые объекты, не только Spring-бины — AspectJ load-time weaving.

В практике backend-сервисов почти никогда не нужно — Spring AOP покрывает 95% случаев.

Что почитать дальше

  • @Transactional глубоко — главный пример Spring AOP, со всеми ограничениями.
  • DI/IoC, bean lifecycle, scopes — где именно в lifecycle создаются AOP-прокси.
  • Spring Security — @PreAuthorize через AOP.
  • Actuator, метрики, трейсинг — Micrometer Observation как замена custom-aspect'ов для метрик/трейсинга.