Що таке 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).
Коли клієнт викликає метод на проксі-об’єкті:
- Контейнер формує список інтерцепторів (
List<MethodInterceptor>), які відповідають цьому методу. - Створюється екземпляр
ReflectiveMethodInvocation. - Реалізується патерн «Цибулинна архітектура» (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.