1. @Aspect і увімкнення AOP
Якщо ви вперше пишете аспект, майже гарантовано станеться невелика драма: ви додаєте @Aspect, запускаєте застосунок, чекаєте на красиві логи… і отримуєте тишу. Це не тому, що Spring вас ігнорує. Просто аспект — це не магічне заклинання, а звичайний bean, якому потрібна відповідна інфраструктура: контейнер має навчитися створювати проксі, знаходити аспекти й обгортати потрібні bean-и. Без явного увімкнення AOP-механіки аспект залишиться просто «ще одним класом», яким ніхто не користується.
Уявіть, що ви поставили в кімнаті пожежну сигналізацію (клас із @Aspect), але забули підʼєднати її до електрики (@EnableAspectJAutoProxy). Сам блок гарно висить, але пожежа йому абсолютно байдужа.
Тут важлива одна думка: щоб аспект почав справді обгортати виклики, потрібно увімкнути proxy-based AOP у чистому Spring-контексті й написати around-advice, який правильно викликає цільовий метод через proceed().
Залежності для Spring AOP
Перед тим як писати код, корисно швидко перевірити реальність: чи є в проєкті потрібні залежності. У ContextFlow вони вже мають бути підключені, але новачку дуже корисно побачити, що AOP — це не «вбудовано в Java», а окрема частина стека.
Нам потрібні дві речі. По-перше, модуль Spring, який уміє будувати AOP-проксі (spring-aop). По-друге, бібліотека AspectJ, з якої беруться класи на кшталт ProceedingJoinPoint і анотації @Aspect (aspectjweaver у нашому baseline). Ми не робимо compile-time weaving і не перетворюємо проєкт на «курси AspectJ», але ця бібліотека потрібна як runtime-частина.
Мінімальний фрагмент build.gradle.kts (показую лише важливе, решта у вас уже є):
dependencies {
// Базовий Spring-контейнер: конфігурація, сканування компонентів, життєвий цикл bean-ів
implementation("org.springframework:spring-context")
// Spring AOP: уміє будувати проксі та застосовувати advice за pointcut-ами
implementation("org.springframework:spring-aop")
// AspectJ runtime: анотації та API (ProceedingJoinPoint тощо) для @Aspect
implementation("org.aspectj:aspectjweaver")
}
Якщо у своєму проєкті ви бачите саме це (або те саме, але через BOM-узгодження) — чудово. Якщо чогось бракує, симптом буде або компіляційний (класи org.aspectj.* не знаходяться), або «аспект є, але не працює» (AOP-інфраструктура не активована).
3. Увімкнення AOP: @EnableAspectJAutoProxy
Тепер робимо головний крок: кажемо Spring-контейнеру, що можна створювати AOP-проксі та застосовувати аспекти. Для цього існує анотація @EnableAspectJAutoProxy. Вона не робить магію напряму; вона вмикає інфраструктуру, яка під час старту контейнера (через знайому вам модель BeanPostProcessor) перевірить bean-и й за потреби замінить їх на proxy-обʼєкти.
Тобто ми не додаємо «ще одну бібліотеку логування». Ми вмикаємо механізм, який буде вбудовуватися в створення bean-ів. Це важливо: AOP у Spring — частина контейнерного життєвого циклу, а не static-хак у вашому сервісі.
У навчальному варіанті ми можемо увімкнути AOP прямо в нашому конфігураційному класі верхнього рівня:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
// Важливо: скануємо кореневий пакет, щоб знайшлися і сервіси, і аспекти
@ComponentScan("com.example.contextflow")
// Вмикаємо інфраструктуру Spring AOP (автоматичне створення proxy для відповідних bean-ів)
@EnableAspectJAutoProxy
public class AppConfig {
// Конфігурація може бути порожньою — нам важливі анотації та увімкнення інфраструктури
}
Зверніть увагу на одну методичну деталь: @ComponentScan("com.example.contextflow") покриває і application.service, і support.aop. Це означає, що контейнер знайде й клас аспекту. Якщо ви випадково обмежите сканування лише application, аспект просто не зареєструється, і далі ви можете хоч тричі вмикати AOP — застосовувати буде нічого.
Якщо ваш проєкт уже виріс до модульної конфігурації (що нормально для ContextFlow після теми про @Import), практичніше тримати увімкнення AOP окремим модулем, наприклад AopConfig, і імпортувати його в основний конфіг. Але принцип той самий: десь у вашій Java-конфігурації обовʼязково має бути @EnableAspectJAutoProxy.
Ось дуже спрощена схема того, що ми зараз увімкнули:
flowchart TD
A[Запуск ApplicationContext] --> B[Створення інфраструктурних bean-ів]
B --> C["@EnableAspectJAutoProxy реєструє AutoProxyCreator (BPP)"]
C --> D[Контейнер створює ваші bean-и]
D --> E[AutoProxyCreator вирішує: потрібен proxy чи ні]
E --> F[Bean у контексті стає proxy]
F --> G[Виклики йдуть через advice -> target]
Нехай ця схема вас не лякає: вам не треба запамʼятовувати назви внутрішніх класів. Важливо розуміти причинно-наслідковий звʼязок: AOP = інфраструктура контейнера + proxy + перехоплення виклику методу.
4. Перший @Around-аспект і proceed()
Найкоротший шлях відчути користь AOP — зробити аспект, який вимірює час виконання методів сервісного шару. Ми вже бачили ручний варіант у минулій лекції: start = nanoTime(), try/finally, виведення. Тепер винесемо це в окремий клас і підключимо його через контейнер до всіх відповідних методів.
Створимо аспект ServiceTimingAspect (за структурою проєкту — логічно в com.example.contextflow.support.aop):
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect // Кажемо: це аспект, усередині є advice-методи
@Component // Реєструємо аспект як Spring bean (інакше контейнер його не побачить)
public class ServiceTimingAspect {
// Pointcut: усі методи всіх класів у пакеті application.service та його підпакетах
@Around("execution(* com.example.contextflow.application.service..*.*(..))")
public Object timing(ProceedingJoinPoint pjp) throws Throwable {
// Засікаємо час до виклику бізнес-методу
long start = System.nanoTime();
try {
// ВАЖЛИВО: proceed() реально запускає цільовий метод
return pjp.proceed();
} finally {
// finally гарантує лог навіть у разі винятку в бізнес-методі
long tookNs = System.nanoTime() - start;
// Підпис методу допомагає зрозуміти, що саме було викликано
System.out.println(pjp.getSignature().toShortString()
+ " took " + tookNs + " ns"); // generateDailyReport(..) виконано за 152300 нс
}
}
}
Давайте розберемо цей код як дорослі люди, які не хочуть вчити анотації напамʼять, а хочуть розуміти, що відбувається.
@Aspect говорить Spring-інфраструктурі: «це аспект, у ньому є advice-методи». Але сам по собі @Aspect не вмикає AOP — це лише позначка. Саме тому нам і знадобився @EnableAspectJAutoProxy.
@Component робить аспект звичайним Spring bean-ом. Без цього контейнер його не побачить, якщо ви не зареєструєте аспект через @Bean вручну.
@Around(...) — це around-advice. Його зміст у тому, що він буквально обгортає виклик цільового методу. До proceed() — «до виклику», після proceed() — «після виклику».
ProceedingJoinPoint — це обʼєкт, через який advice отримує доступ до поточного виклику. І так, proceed() — це та сама кнопка «продовжити виконання». Якщо її не натиснути, цільовий метод не виконається взагалі. Це типова помилка новачка: написати аспект, який «усе логує», і випадково перетворити застосунок на театр тіней без реальної дії.
Окремо відзначу System.nanoTime(). Для вимірювання тривалості це правильніше, ніж currentTimeMillis(), тому що nanoTime() монотонний і не залежить від того, що зміниться системний годинник або синхронізується час. Нам важлива не поточна дата, а різниця.
У цій конструкції вже зібрана вся основа AOP-сценарію: сервіс, керований контейнером, увімкнений @EnableAspectJAutoProxy і один ServiceTimingAspect, який обгортає сервісний шар через @Around. Тепер змінюватися можуть лише дві речі: наскільки широко pointcut вибирає методи і яким шляхом виклик дістається до target-обʼєкта.
5. Перевірка роботи через proxy
AOP — тема, де дуже легко обманути себе. Тому ми перевіряємо не «мені здається», а «я бачу це у виводі». Для простоти візьмемо будь-який сервісний bean із application.service. Нехай це буде ReportingService з коротким методом, щоб вивід був очевидним.
Мінімальний приклад сервісу:
import org.springframework.stereotype.Service;
@Service // Цей клас має бути Spring bean-ом, щоб його можна було проксувати
public class ReportingService {
public void generateDailyReport() {
// Це контрольне повідомлення: воно допомагає зрозуміти, що сам метод справді виконувався
System.out.println("Формуємо звіт..."); // Формуємо звіт...
}
}
А тепер правильний запуск — саме через ApplicationContext, щоб контейнер міг підставити proxy:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApp {
public static void main(String[] args) {
// Контекст керує створенням bean-ів, і саме тут вмикається AOP-проксування
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Беремо bean із контексту (ймовірно, це proxy, а не "чистий" ReportingService)
ReportingService service = context.getBean(ReportingService.class);
// Виклик піде через proxy -> advice -> target-метод
service.generateDailyReport();
}
}
}
У консолі ви повинні побачити два повідомлення. Перше — від самого сервісу, друге — від аспекту:
Формуємо звіт...
generateDailyReport(..) виконано за 152300 нс
Якщо ви бачите лише Формуємо звіт..., то AOP не увімкнувся. Найчастіші причини ми розберемо наприкінці лекції, але логіка така: або не увімкнули @EnableAspectJAutoProxy, або аспект не зареєструвався як bean, або pointcut не збігся, або ви викликали метод не через bean із контексту.
І ось тут корисно показати анти-приклад, щоб мозок закріпив правило. Якщо ви створите обʼєкт вручну через new, аспект не спрацює, тому що Spring взагалі не бере участі у створенні й не може підсунути proxy:
public class ManualRunDemo {
public static void main(String[] args) {
// Створили обʼєкт вручну — Spring-контейнер не бере участі, proxy не зʼявиться
ReportingService service = new ReportingService();
// Виклик іде напряму, advice ніколи не відпрацює
service.generateDailyReport(); // Формуємо звіт...
}
}
У цьому прикладі все чесно: ви самі створили обʼєкт, самі його викликали, Spring стоїть осторонь і лише спостерігає. AOP — це історія про container-managed beans, а не про будь-які обʼєкти JVM.
Дані з ProceedingJoinPoint
Коли around-advice запрацював, зʼявляється природна спокуса: «О, я тепер бачу все! Зараз роздрукую аргументи, результати, весь Всесвіт!» Тут варто зупинитися й згадати, що ми робимо навчальний аспект для timing, а не систему тотального стеження за колегами (хоча… жарт).
ProceedingJoinPoint дає трохи корисних даних, і їх достатньо, щоб зробити висновок зрозумілішим. Наприклад, замість короткого generateDailyReport(..) можна вивести більш архітектурну назву:
// Повна сигнатура зручна, коли в логах багато методів із однаковими іменами
String method = pjp.getSignature().toLongString();
System.out.println(method);
// public void com.example.contextflow.application.service.ReportingService.generateDailyReport()
Або можна вивести імʼя класу target-обʼєкта (іноді це корисно, тому що pjp.getThis() і pjp.getTarget() у світі proxy можуть відрізнятися):
// getTarget() — "реальний" обʼєкт, який обгортає проксі
Object target = pjp.getTarget();
System.out.println("Target class = " + target.getClass().getName());
// Target class = com.example.contextflow.application.service.ReportingService
А з аргументами (pjp.getArgs()) краще бути обережнішими. У навчальному проєкті це безпечно, але в реальному застосунку дуже легко випадково залогувати щось зайве. На рівні курсу нам достатньо знати, що така можливість є, і памʼятати правило: технічний лог не повинен перетворюватися на витік даних.
6. Де жити аспектам у ContextFlow
Тепер найпрактичніше: як вбудувати цей механізм так, щоб проєкт не перетворився на «випадковий набір аспектів», а залишився читабельним. У ContextFlow у нас є зручне місце для таких речей — пакет support.aop. Сервіси живуть у application.service, доменні моделі — у domain.model. І це не просто краса: так ви фізично відділяєте «технічну механіку» від «бізнес-сенсу».
Аспект ServiceTimingAspect — це чиста інфраструктура. Він не повинен знати про Order, Customer або «знижка для лояльних клієнтів». Йому байдуже, що саме робить сервіс — він вимірює час і пише технічну діагностику. Якщо вам раптом захотілося всередині аспекту написати «якщо замовлення скасовано, то…» — це сигнал, що ви змішали доменну логіку з технічною. У такому разі краще відкласти клавіатуру й тихо запитати себе: «Я точно не намагаюся запхати бізнес-сценарій в аспект із лінощів?»
Технічно для підключення до наявного проєкту зазвичай достатньо двох дій: додати @EnableAspectJAutoProxy в головний конфіг (або імпортувати AOP-модуль конфігурації) і переконатися, що support.aop потрапляє в @ComponentScan. Якщо кореневий пакет застосунку — com.example.contextflow, сканування від нього захопить усе потрібне.
У результаті сервісний шар залишається чистим: у методах немає nanoTime(), немає try/finally, немає повторюваних рядків. І водночас ви отримуєте спостережуваність поведінки застосунку, що особливо приємно, коли сервісів стає більше, а сценарії довші.
7. Типові помилки під час роботи зі Spring AOP
У цій темі помилки зазвичай не «тонкі», а дуже прямолінійні: ви або забули увімкнути інфраструктуру, або написали advice так, що він не викликає цільовий метод, або взагалі не потрапили в pointcut. Добра новина: усе це лікується не шаманством, а перевіркою кількох конкретних місць.
Помилка №1: забули @EnableAspectJAutoProxy.
Симптом виглядає особливо підступно: аспект як клас існує, як bean може навіть піднятися (якщо він @Component), застосунок запускається, але у виводі немає жодних слідів advice. Причина проста: контейнер не отримав «дозволу» створювати AOP-проксі, і ніякого auto-proxying не відбувається.
Помилка №2: аспект не став Spring bean-ом.
@Aspect — не реєстрація. Якщо ви прибрали @Component і не додали @Bean у конфігурацію, контейнер не побачить аспект. У результаті AOP увімкнено, але застосовувати нічого. Важливо памʼятати: аспект у Spring — це звичайний bean.
Помилка №3: у @Around забули викликати proceed().
Це класика жанру. Здається, усе працює, advice друкує повідомлення, але бізнес-метод не виконується. В around-advice ви зобовʼязані викликати pjp.proceed() (або свідомо замінити виконання на щось інше — але це точно не наш навчальний сценарій).
Помилка №4: advice не повертає результат proceed().
Якщо цільовий метод повертає значення, а ви в advice повертаєте null або взагалі нічого, ви ламаєте контракт методу. У кращому разі отримаєте NullPointerException у коді, який викликає цей метод, у гіршому — тихі логічні помилки. Для навчального варіанта за замовчуванням: return pjp.proceed(); і лише потім ускладнювати.
Помилка №5: ви викликаєте сервіс не з контексту, а через new.
AOP працює лише на container-managed bean-ах. Якщо ви створили обʼєкт вручну, Spring не підклав proxy, advice не спрацює. Симптом зазвичай звучить як «ну чому аспект не працює, я ж написав анотації?!», а відповідь нудна: тому що анотації — це не магія, а метадані для контейнера.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ