🍃 Раздел 5 · Вопрос #15

Что такое AOP (Aspect-Oriented Programming)

В любом корпоративном приложении есть задачи, которые не относятся к прямой предметной области (бизнес-логике), но требуются почти во всех слоях системы:


🟢 Junior Level

AOP (Aspect-Oriented Programming, аспектно-ориентированное программирование) — это парадигма разработки, предназначенная для выделения сквозной функциональности (Cross-Cutting Concerns) из основной бизнес-логики приложения.

Что такое сквозная функциональность

В любом корпоративном приложении есть задачи, которые не относятся к прямой предметной области (бизнес-логике), но требуются почти во всех слоях системы:

  • Управление транзакциями (открытие, коммит, откат).
  • Логирование и аудит операций.
  • Проверка прав доступа и авторизация (Security).
  • Мониторинг времени выполнения и сбор метрик.
  • Кэширование ответов.

Без AOP (Спагетти-код): Код логирования, проверки прав и транзакций копируется в начало и конец каждого метода. С AOP (Декларативный подход): Бизнес-метод содержит только предметную логику, а сквозные задачи выносятся в отдельный класс — Аспект (Aspect), который подключается декларативно (через аннотацию).

Базовый пример

package com.example.service;

import com.example.aspect.LogExecutionTime;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    // Сквозная логика вынесена в аннотацию
    @LogExecutionTime
    public void processOrder(Long orderId) {
        // Чистая бизнес-логика без таймеров и логгеров
        System.out.println("Обработка заказа " + orderId);
    }
}
package com.example.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("@annotation(LogExecutionTime)")
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            // Вызов реального метода
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.println("Метод " + joinPoint.getSignature().getName() 
                    + " выполнен за " + duration + " ms");
        }
    }
}

🟡 Middle Level

4 ключевых термина AOP

Join Point (Где возможно)  ──> Любая доступная точка выполнения (в Spring — только вызов метода).
Pointcut (Где именно)       ──> Срез/фильтр, определяющий конкретные Join Points по выражению.
Advice (Что сделать)       ──> Код сквозной логики (до, после, вокруг метода).
Aspect (Аспект)            ──> Модуль, объединяющий Pointcut и Advice в единый бин.

5 типов советов (Advice)

Аннотация Когда выполняется Особенности
@Before До входа в целевой метод Не может изменить возвращаемое значение или отменить вызов (если не выбросит исключение).
@AfterReturning После успешного завершения Имеет доступ к возвращенному значению через параметр returning.
@AfterThrowing Только если метод выбросил исключение Имеет доступ к объекту исключения через throwing.
@After (finally) После любого завершения (успех или ошибка) Аналог блока finally в Java.
@Around Оборачивает вызов метода целиком Самый мощный совет. Может перехватить параметры, изменить результат, обработать ошибки или вообще не вызывать целевой метод.

Сравнение: Spring AOP vs Полноценный AspectJ

Критерий Spring AOP (из коробки) AspectJ (полная реализация)
Реализация Динамические прокси (CGLIB / JDK Dynamic Proxy) Модификация байткода (Weaving)
Точки внедрения (Join Points) Только выполнение public-методов Spring-бинов Любые методы, приватные методы, поля, конструкторы
Self-invocation (this.method()) ❌ Не работает (вызов идет в обход прокси) ✅ Работает (код внедрен прямо в байткод класса)
Зависимость от Spring Работает только с бинами внутри ApplicationContext Работает с любыми POJO (даже созданными через new)
Сложность внедрения Минимальная (просто включить @EnableAspectJAutoProxy) Требует настройки плагинов компилятора или Java-агента
Производительность Накладные расходы прокси (~микросекунды) Максимальная (прямой скомпилированный вызов)

🔴 Senior Level

Архитектура выполнения перехвата: Цепочка интерцепторов

В Spring AOP понятие аспекта внутренне преобразуется в объект Advisor (сочетание Pointcut + Advice).

Когда клиент вызывает метод на прокси-объекте:

  1. Контейнер формирует список интерцепторов (List<MethodInterceptor>), подходящих под данный метод.
  2. Создается экземпляр ReflectiveMethodInvocation.
  3. Реализуется паттерн «Луковичная архитектура» (Onion Architecture) через рекурсивный вызов метода proceed():
[Вызов клиента] 
       ↓
 ┌─────────────────────────────────────────────────────────────┐
 │ Аспект 1: SecurityInterceptor (@Order(1))                   │
 │   ┌─────────────────────────────────────────────────────────┐
 │   │ Аспект 2: TransactionInterceptor (@Order(2))            │
 │   │   ┌─────────────────────────────────────────────────────┐
 │   │   │ Аспект 3: CustomLoggingAspect (@Order(3))           │
 │   │   │   ┌─────────────────────────────────────────────────┐
 │   │   │   │ Target Object (Целевой бизнес-метод)            │
 │   │   │   └─────────────────────────────────────────────────┘
 │   │   │ Выход из CustomLoggingAspect                        │
 │   │   └─────────────────────────────────────────────────────┘
 │   │ Transaction commit / rollback                           │
 │   └─────────────────────────────────────────────────────────┘
 │ Security context cleanup                                    │
 └─────────────────────────────────────────────────────────────┘
       ↓
[Возврат клиенту]

Порядок аспектов (@Order / Ordered)

Если к одному методу применимо несколько аспектов, порядок их оборачивания критичен:

@Aspect
@Component
@Order(1) // Выполняется ПЕРВЫМ при входе и ПОСЛЕДНИМ при выходе
public class SecurityAspect { ... }

@Aspect
@Component
@Order(2) // Выполняется ВТОРЫМ (внутри блока Security)
public class TransactionalAspect { ... }

Правило: Меньшее значение @Order означает более высокий приоритет (внешний слой луковицы).

Потокобезопасность аспектов (Thread Safety)

По умолчанию классы аспектов (@Aspect) регистрируются как Singleton-бины:

  • Один и тот же экземпляр аспекта вызывается параллельно сотнями потоков HTTP-сервлетов.
  • Категорически запрещено хранить изменяемое состояние метода в полях аспекта!
    @Aspect
    @Component
    public class DangerousAspect {
    
      // ОШИБКА: Поле класса аспекта разделяется всеми потоками!
      // Возникает Data Race и утечка контекста между пользователями.
      private Long currentUserId; 
    }
    

    Любые контекстные данные должны передаваться исключительно через параметры методов или через ThreadLocal / RequestContextHolder.

Spring 6, Java 21 и Virtual Threads: Угроза Carrier Thread Pinning

При переходе на Virtual Threads (Project Loom в Java 21+):

  • Если код кастомного аспекта или интерцептора содержит блоки synchronized (например, синхронизация счетчиков вызовов или локального кэша):
    @Around("execution(* com.example..*(..))")
    public Object badSync(ProceedingJoinPoint pjp) throws Throwable {
        synchronized (lock) { // ОПАСНО ДЛЯ VIRTUAL THREADS!
            return pjp.proceed();
        }
    }
    
  • Виртуальный поток блокирует (pins) базовый системный поток ОС (Carrier Thread из ForkJoinPool).
  • При высокой нагрузке пул базовых потоков исчерпывается, и приложение встает в дедлок (Starvation).
  • Решение: использовать java.util.concurrent.locks.ReentrantLock вместо блоков synchronized.

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

30-секундный ответ

AOP (Aspect-Oriented Programming) — парадигма вынесения сквозной функциональности (транзакции, безопасность, кэширование, логирование) из бизнес-кода. В Spring AOP реализуется через динамические прокси в рантайме (CGLIB / JDK Proxy) и применяется только к public-методам Spring-бинов. Основные элементы:

  • Join Point: точка перехвата (вызов метода).
  • Pointcut: выражение, отбирающее нужные точки.
  • Advice: исполняемый код (@Before, @After, @AfterReturning, @AfterThrowing, @Around).
  • Aspect: класс, объединяющий Pointcut и Advice. Полноценный AspectJ отличается от Spring AOP модификацией байткода (weaving) и способностью перехватывать приватные методы, конструкторы и self-invocation.

4 каверзных вопроса с ответами

  1. В чём ключевая разница между Spring AOP и полноценным AspectJ? Ответ: Spring AOP работает через динамические прокси (CGLIB/JDK) во время выполнения и поддерживает перехват только public-методов Spring-бинов. AspectJ использует модификацию байткода (Weaving на этапе компиляции, постобработки или загрузки через Java-агент), позволяя перехватывать любые методы (включая private), конструкторы, доступ к полям и вызовы внутри одного класса (self-invocation).

  2. Как регулируется порядок выполнения, если на один метод назначено несколько аспектов? Ответ: С помощью аннотации @Order(value) или реализации интерфейса Ordered. Аспект с меньшим значением порядка (@Order(1)) оборачивает аспекты с большим значением (@Order(2)). Он выполняется раньше при входе в метод и позже при выходе из него (подобно фильтрам сервлетов).

  3. Почему в классах с аннотацией @Aspect нельзя объявлять неконстантные поля? Ответ: Аспекты в Spring по умолчанию являются синглтонами (Singleton). Один и тот же экземпляр аспекта обрабатывает вызовы из множества параллельных потоков. Хранение состояния вызова в поле аспекта приведет к состоянию гонки (Race Condition) и повреждению данных.

  4. Что произойдет, если в совете @Around не вызвать joinPoint.proceed()? Ответ: Целевой бизнес-метод не выполнится вообще. Совет @Around полностью заменяет собой вызов метода. Если proceed() опущен, результатом метода станет значение, возвращенное самим советом (или null).

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Spring AOP изменяет скомпилированный байткод классов на диске» — Spring AOP работает исключительно через прокси-обертки в памяти.
  • ❌ «Spring AOP может перехватывать вызовы private-методов» — прокси не могут перехватывать приватные методы.
  • ❌ «Совет @Around выполняет только код логирования и не может повлиять на результат метода» — @Around может подменять параметры, модифицировать результат и глушить исключения.
  • ❌ «Аспекты создаются заново для каждого потока выполнения» — аспекты являются стандартными синглтонами Spring.

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