JavaRush /Курси /Spring Core /AOP у Spring: перший @Aroun...

AOP у Spring: перший @Around-аспект

Spring Core
Рівень 22 , Лекція 2
Відкрита

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 не спрацює. Симптом зазвичай звучить як «ну чому аспект не працює, я ж написав анотації?!», а відповідь нудна: тому що анотації — це не магія, а метадані для контейнера.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ