JavaRush /Курси /Spring Core /Словник Spring AOP

Словник Spring AOP

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

1. Терміни AOP без містики

Ми вже бачили, як технічне вимірювання часу й логування розповзаються по сервісних методах і перетворюють сценарій на бутерброд з обвʼязки. Тепер корисно назвати частини цієї конструкції по іменах: де саме відбувається втручання, як обираються потрібні методи, що саме виконується навколо виклику та де вся ця логіка живе в коді.

AOP — це той рідкісний випадок, коли нові слова потрібні не для краси, а щоб ви могли взагалі осмислити, що відбувається, і налагодити це. Без словника ви будете говорити «ну воно якось само обгорнулося», а це прямий шлях до містики, сліз і пошуку винного в Місяці та ретроградному Меркурії.

Найважливіше: AOP-терміни описують уже знайому вам proxy-картинку, просто під різними кутами. Вони відповідають на чотири прості запитання. Де саме ми втручаємося (це буде join point), як обираємо місця втручання (це pointcut), що саме робимо навколо виклику (це advice) і де все це живе в коді (це aspect). І ось коли ці слова стають «своїми», Spring AOP перестає виглядати як чорна магія й перетворюється на нормальний інженерний інструмент.

Щоб зафіксувати термінологію, тримайте маленьку «карту місцевості»:

Термін Простими словами Як це виглядає в Spring AOP
Join point конкретне «місце» у виконанні програми, де можна втрутитися у нашому курсі — виконання методу Spring beanʼа
Pointcut правило відбору join pointʼів найчастіше рядок на кшталт execution(...)
Advice код, який виконується навколо вибраного join point часто метод із @Around (ми вчитимемося саме на ньому)
Aspect клас, який збирає pointcutʼи та advice в одну «тему» клас із @Aspect (і він же Spring bean)

2. Proxy-модель і місце AOP

Давайте на хвилину повернемося до картинки «proxy → target». Важливо не просто памʼятати визначення, а чітко бачити, де в цьому ланцюжку зʼявляється AOP. Коли ви викликаєте метод на beanʼі, ви часто викликаєте його не на справжньому обʼєкті сервісу, а на обʼєкті-проксі, який стоїть «перед ним», як турнікет у метро: через нього проходять усі виклики, і він може щось зробити до та після.

У чистій Java без Spring ви зазвичай викликаєте метод напряму: є обʼєкт ReportingService, ви викликаєте generateDailyReport(), метод виконується — усе чесно й просто. У Spring-контейнері часто зʼявляється додатковий учасник: proxy, який вирішує, чи треба «обгорнути» виклик. І AOP — це якраз набір правил, за якими проксі розуміє: «цей метод треба обгорнути», а також набір дій, які треба виконати навколо виклику.

Міні-демо (без AOP-коду, просто щоб нагадати думку «виклик має пройти через контейнер»):

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Demo {
    public static void main(String[] args) {
        // Важливо: беремо bean зі Spring-контейнера, а не створюємо його через new.
        // Лише так у Spring зʼявляється точка, через яку він зможе підставити proxy,
        // якщо AOP-інфраструктуру увімкнено.
        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var service = ctx.getBean(ReportingService.class);

            // Після ввімкнення AOP і збігу pointcut тут може зʼявитися proxy-клас.
            // Саме в такому proxy живе AOP-ланцюжок.
            System.out.println(service.getClass());
        }
    }
}

Сам факт ctx.getBean(...) ще не гарантує proxy сам по собі. Контейнер може повернути й звичайний bean, якщо жодна інфраструктура не обгортає його додатково. Важлива інша думка: лише кероване контейнером посилання дає Spring місце, через яке він узагалі зможе потім пропустити виклик через proxy та advice.

Сенс цього фрагмента не в точній назві класу (вона може відрізнятися), а у відчутті: «ага, контейнер може дати мені не вихідний клас, а обгортку». А тепер головне: AOP якраз живе в цій обгортці.

3. Join point: що перехоплюємо

Слово join point легко звучить як щось грандіозне, ніби Spring уміє перехоплювати «будь-який момент вашого життя». На практиці, і це хороша новина для новачка, у Spring AOP ми майже завжди говоримо про дуже конкретну річ: виконання методу на Spring-managed beanʼі.

Тобто join point — це не «рядок коду», не «умова в методі» і не «момент перед створенням обʼєкта». Це подія рівня runtime: «ось зараз викликається метод generateDailyReport() у beanʼа reportingService». Якщо цей виклик приходить у проксі, у AOP зʼявляється шанс втрутитися: виконати технічну обвʼязку до, після або навколо.

Уявіть, що в нас є сервіс звітів у ContextFlow. Навіть якщо метод усередині невеликий і друкує в консоль, для AOP це все одно нормальна ціль:

import org.springframework.stereotype.Service;

@Service
public class ReportingService {

    public void generateDailyReport() {
        // Приклад бізнес-методу: це і є цільова логіка, яку AOP буде обгортати.
        System.out.println("звіт згенеровано"); // виводимо в консоль факт створення звіту
    }
}

Коли ви викликаєте generateDailyReport() через посилання на bean із контейнера, потенційний join point — це саме виконання методу. Усередині нього Spring AOP не «влізає» в кожен рядок, він не робить магію на рівні операторів Java. Він працює з межею методу, тому що це саме той рівень, який зручно обгортати proxy-механізмом.

Дуже корисний «анти-міф» у цьому місці: join point у Spring AOP на нашому рівні — це не будь-яка ділянка коду. Це саме method execution — виконання методу. Щойно ви це приймаєте, половина майбутніх запитань «чому воно не спрацювало?» починає відповідатися сама собою.

4. Pointcut: правило відбору

Тепер, коли ми розуміємо, що таке join point, виникає наступне чесне запитання: а як обрати, які методи ми будемо обгортати, а які — ні? Ми ж не хочемо, щоб наш замір часу раптово обгорнув половину інфраструктури, а заодно ще й друк банерів на старті застосунку. Ось тут і зʼявляється pointcut.

Pointcut — це фільтр, тобто правило відбору join pointʼів. Він не вимірює час, не пише лог і не змінює результат. Він просто відповідає на запитання: «чи підходить цей конкретний виклик методу під наше правило?». Якщо підходить — advice спрацює. Якщо ні — проксі просто пропустить виклик далі як звичайний виклик.

На цьому етапі нам достатньо розуміти базову форму pointcut-виразу, яку ви найчастіше побачите в прикладах Spring AOP: execution(...). Наприклад, такий pointcut «націлений» на сервісний шар нашого проєкту:

// Pointcut-вираз найчастіше живе всередині анотацій на кшталт @Around("...").
// Тут це просто рядок, щоб побачити форму.
String pointcut = "execution(* com.example.contextflow.application.service.*.*(..))";

// Для наочності друкуємо вираз, щоб він «зафіксувався очима».
System.out.println(pointcut); // execution(* com.example.contextflow.application.service.*.*(..))

Давайте прочитаємо це по-людськи, не перетворюючи лекцію на курс із мови виразів.

Усередині execution(...) зазвичай закодовано пʼять шматочків інформації: тип поверненого значення (тут *, тобто будь-який), пакет і клас (тут ...application.service.* — будь-які класи в пакеті), імʼя методу (тут * — будь-яке) та аргументи (тут (..) — будь-які аргументи). Переклад виходить приблизно такий: «обери виконання будь-якого методу будь-якого класу з пакета сервісів».

Це схоже на правило для охоронця: «Пропускаємо всіх, хто є в списку працівників». Охоронець не виконує роботу працівника, він лише вирішує, кому можна зайти. Точно так само pointcut не робить замір часу — він каже advice: «так, тут твій вихід на сцену».

Наше завдання поки інше: навчитися бачити в pointcut правило відбору, а не «шматок коду». Поки цього достатньо: щойно видно саму ідею фільтра, межу вже можна свідомо звужувати або розширювати під потрібний шар застосунку.

5. Advice: обвʼязка навколо методу

Якщо pointcut — це фільтр, то advice — це вже реальна дія, тобто код, який виконуватиметься навколо виклику методу. І тут дуже важливо не переплутати ролі: pointcut відповідає на запитання «де», advice — «що робимо», а join point — це «ось конкретно цей виклик методу».

У Spring AOP є кілька типів advice, на кшталт «до», «після», «після успішного виконання» та «після винятку». Але для навчання новачка найчесніший і найнаочніший шлях — @Around, тому що він показує всю механіку обгортання: ми можемо виконати код до виклику, викликати цільовий метод, а потім виконати код після виклику.

Щоб не забігати наперед і не писати повноцінний аспект, а це буде наступна лекція, давайте подивимося на каркас around-логіки як на звичайну ідею. Уявіть, що в нас є абстрактний «виклик методу», який ми хочемо обгорнути:

import org.aspectj.lang.ProceedingJoinPoint;

public class AroundIdea {

    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // Фіксуємо момент перед викликом цільового методу.
        long start = System.nanoTime();

        try {
            // ВАЖЛИВО: саме proceed() запускає «справжній» метод (target).
            // Якщо його не викликати — цільовий метод узагалі не виконається.
            return pjp.proceed();
        } finally {
            // finally гарантує, що ми зафіксуємо час навіть у разі винятку.
            System.out.println("зайняло " + (System.nanoTime() - start) + " нс");
        }
    }
}

Так, ProceedingJoinPoint — слово довге. Але сенс у нього доволі дружній: це обʼєкт, через який advice може сказати: «а тепер виконай той метод, який ми обгортаємо». Саме proceed() — це пусковий гачок виконання target-методу.

Корисно запамʼятати одразу два практичні факти. Якщо ви не викличете proceed(), цільовий метод не виконається взагалі, і ви будете довго дивитися на порожню консоль, думаючи, що сервіс зламано. Якщо ви не повернете результат proceed() для методів із return value, ви зламаєте контракт методу й отримаєте неочікувані null або помилки типів. Тобто around-advice — це не «декорація», а реальна частина ланцюжка виклику.

6. Aspect: pointcut + advice

Тепер зберемо поняття в один клас. Aspect — це місце, де живуть pointcutʼи та advice, обʼєднані спільною ідеєю. Наприклад, «вимірювання часу сервісів» — це одна ідея. «Технічне логування викликів» — інша. «Перевірка прав» — третя. Важливо, що аспект — це не «щось поза Spring». У Spring він зазвичай є звичайним beanʼом, просто з додатковою роллю.

Мінімальний порожній аспект у нашому проєкті може виглядати так:

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

@Aspect // Позначаємо клас як AOP-аспект: усередині будуть правила pointcut/advice
@Component // Реєструємо як Spring bean, інакше аспект «не побачать» проксі
public class ServiceTimingAspect {
    // Тут пізніше зʼявляться методи з @Around / @Before / @After тощо.
}

Тут поки немає ні pointcut, ні advice — лише роль. Анотація @Aspect говорить: «у цьому класі будуть AOP-правила». Анотація @Component (або реєстрація через @Bean) говорить контейнеру: «це треба побачити й створити як bean». І це дуже важлива звʼязка: без реєстрації як bean аспект не бере участі в житті контейнера, а отже не може впливати на виклики.

Є тонкий момент, який дуже корисно тримати в голові вже зараз: наявність класу аспекту ще не гарантує, що proxy почнуть застосовуватися саме як AOP. Для цього в чистому Spring потрібно увімкнути AOP-інфраструктуру; інакше клас із @Aspect так і залишиться звичайним beanʼом із додатковою анотацією. Але навіть до цього кроку важливо розуміти: аспект — це не магічний файл конфігурації, а клас у вашому проєкті, який живе за правилами DI, scope і lifecycle як звичайний bean.

7. Ланцюжок виклику в AOP

Зараз зберемо всі терміни назад у вже знайому proxy-картинку. Якщо в цей момент «клацне», далі буде набагато простіше читати документацію й дебажити поведінку застосунку. А якщо не клацне — нічого страшного, це як із велосипедом: на словах усе зрозуміло, але баланс приходить лише в практиці.

Уявіть, що ScenarioRunner (або будь-який код сценарію) викликає метод сервісу. У контейнері є proxy, усередині якого живе AOP-логіка. Proxy дивиться: чи підходить виклик під pointcut. Якщо так — запускає advice. Advice всередині викликає proceed(), і лише тоді виконання потрапляє в target object, тобто в реальний сервіс.

Ось це ж у вигляді маленької діаграми:

sequenceDiagram
    participant C as "ScenarioRunner (ваш код)"
    participant P as "Proxy (Spring)"
    participant A as "Advice (з Aspect)"
    participant T as "Target (реальний сервіс)"

    C->>P: service.generateDailyReport()
    P->>P: pointcut збігається?
    P->>A: "around(joinPoint)"
    A->>T: "proceed() -> generateDailyReport()"
    T-->>A: return
    A-->>P: return
    P-->>C: return

І ось де кожне слово «сидить» у цьому ланцюжку. Join point — це конкретне «generateDailyReport() зараз виконується». Pointcut — це перевірка: «а це наш join point чи ні?». Advice — це «обгортка», яка виконується навколо. Aspect — це клас, у якому описані ці правила та обвʼязка.

Зверніть увагу: жодної магії всередині класу не відбувається. Метод сервісу не стає іншим методом. Він просто починає виконуватися не напряму, а через додатковий ланцюжок викликів, тому що ви звертаєтеся до нього через proxy. Це саме та інженерна чесність, за яку Spring AOP люблять: ви можете пояснити те, що відбувається, крок за кроком.

8. AOP у ContextFlow

Щоб AOP не перетворився на абстрактну теорію, закріпимо, де все це житиме в нашому наскрізному проєкті ContextFlow. За архітектурою курсу в нас є пакети domain, application, infrastructure, support та config. AOP — це переважно support-механіка, тому що це технічна обвʼязка. Отже, аспект логічно тримати в com.example.contextflow.support.aop.

А ось ціль для AOP у нас — сервісний шар застосунку. У проєкті він живе в com.example.contextflow.application.service. Це зручно ще й тому, що pointcut на рівні пакета стає читабельним: «обгортаємо сервіси». На перших кроках це найзрозуміліший і найстійкіший варіант: обираємо архітектурну межу за пакетом, а не за випадковим набором класів.

Наприклад, ось такий метод цілком собі кандидат на join point для аспекта вимірювання часу:

import org.springframework.stereotype.Service;

@Service
public class OrderPlacementService {

    public void placeOrder() {
        // Приклад цільового методу сервісу: він буде обгорнутий proxy, якщо потрапить під pointcut.
        System.out.println("розміщуємо замовлення"); // виводимо в консоль факт розміщення замовлення
    }
}

А ось доменна модель — не мішень для AOP у нашому курсі, і це навіть добре. Order, OrderItem, команди та події — це звичайні обʼєкти. Вони створюються як частина бізнес-логіки, часто через new, і взагалі не зобовʼязані бути beanʼами. AOP у нашому сценарії не про «все підряд», а про керовані контейнером сервіси.

І ще один важливий момент: не плутайте технічну обвʼязку з бізнес-аудитом. Аудит у нас — частина доменного сценарію: його можна писати через AuditWriter, а публікувати через події та слухачів. Вимірювання часу й технічне логування — це те, що ми будемо виносити в aspect. Якщо ви почнете писати бізнес-сенс в aspect, ви отримаєте «таємний уряд» усередині застосунку: поведінка буде не в сервісах, а десь збоку, і читати систему стане важко.

9. Типові помилки в Spring AOP

Помилка № 1: плутати aspect і advice, вважаючи їх «одним і тим самим».
У розмовній мові легко сказати «аспект спрацював» і мати на увазі «виконався шматок коду до або після виклику». Але в голові краще тримати точніше: аспект — це клас або модуль, а advice — конкретна дія всередині нього. Це дуже допомагає під час читання коду: ви бачите клас ServiceTimingAspect (aspect) і бачите метод measure(...) (advice).

Помилка № 2: думати, що pointcut «робить роботу».
Pointcut — не логіка. Він не вимірює час, не пише лог і не перевіряє права. Він лише вирішує: «цей виклик методу підходить чи ні». Якщо у вас у голові pointcut = «умова», а advice = «дія», ви майже перестаєте плутатися в AOP.

Помилка № 3: сприймати join point як «будь-який рядок коду».
У Spring AOP на нашому рівні join point — це виконання методу на Spring beanʼі. Якщо ви очікуєте, що аспект перехопить приватний метод, виклик усередині конструктора або щось усередині new Order(...), ви розчаруєтеся. Це не баг — це межа proxy-based моделі, і вона чесна.

Помилка № 4: забувати, що вся ця історія працює лише через proxy.
Якщо обʼєкт створено через new, жодного proxy там не зʼявиться, а значить і AOP не спрацює. Це особливо часто спливає в навчальних демо: «я створив сервіс вручну й викликав метод — чому вимірювання часу не спрацювало?». Тому що ви обійшли контейнер, і це буквально те саме, що намагатися пройти в метро не через турнікет.

Помилка № 5: тягнути бізнес-логіку в aspect «раз він усе одно все перехоплює».
Аспект — місце для технічної обвʼязки. Щойно ви починаєте вирішувати в ньому бізнес-питання (наприклад, «якщо замовлення дороге — роби ось так»), ви робите поведінку застосунку неявною. Сервіси перестають бути читабельними, а сценарій починає залежати від «прихованого шару». На перших кроках це може здаватися зручним, але далі зазвичай перетворюється на підтримку питання: «а чому воно так вирішило?».

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