🍃 Розділ 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 {
    
      // ПОМИЛКА: Поле класу аспекту розділяється між усіма потоками!
      // Виникає Race Condition та витік контексту між користувачами.
      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.

Пов’язані теми