1. Термины AOP без мистики
Мы уже увидели, как timing и техническое логирование размазываются по сервисным методам и превращают сценарий в бутерброд из обвязки. Теперь полезно назвать части этой конструкции по именам: где вообще происходит вмешательство, как выбираются нужные методы, что именно выполняется вокруг вызова и где вся эта логика живёт в коде.
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, если никакая инфраструктура не оборачивает его дополнительно. Важна другая мысль: только container-managed ссылка даёт Spring место, через которое он вообще сможет потом пропустить вызов через proxy и advice.
Смысл этого фрагмента не в точном имени класса (оно может отличаться), а в ощущении: «ага, контейнер может дать мне не исходный класс, а обёртку». А теперь главное: AOP как раз живёт в этой обёртке.
3. Join point: что перехватываем
Слово join point легко звучит как что-то грандиозное, будто Spring умеет перехватывать «любой момент вашей жизни». На практике (и это хорошая новость для новичка) в Spring AOP мы почти всегда говорим про очень конкретную вещь: выполнение метода на Spring-managed bean’е.
То есть join point — это не «строчка кода», не «если в методе», не «перед созданием объекта». Это событие уровня рантайма: «вот сейчас вызывается метод generateDailyReport() у bean’а reportingService». Если этот вызов приходит в прокси, у AOP появляется шанс вмешаться: выполнить техническую обвязку до, после или вокруг.
Представьте, что у нас есть сервис отчётов в ContextFlow. Даже если метод внутри небольшой и печатает в консоль, для AOP это всё равно нормальная цель:
import org.springframework.stereotype.Service;
@Service
public class ReportingService {
public void generateDailyReport() {
// Пример «бизнес-метода»: это и есть target-логика, которую AOP будет оборачивать.
System.out.println("report is generated"); // выводим в консоль факт генерации отчёта
}
}
Когда вы вызываете generateDailyReport() через ссылку на bean из контейнера, потенциальный join point — это само выполнение метода. Внутри него Spring AOP не «влезает» в каждую строчку, он не делает магию на уровне операторов Java. Он работает с границей метода, потому что это ровно тот уровень, который удобно оборачивать прокси-механизмом.
Очень полезный “анти-миф” на этом месте: join point в Spring AOP на нашем уровне — это не «любой участок программы». Это именно method execution (выполнение метода). Как только вы это принимаете, половина будущих вопросов «почему оно не сработало?» начинает отвечаться сама собой.
4. Pointcut: правило отбора
Теперь, когда мы понимаем, что такое join point, возникает следующий честный вопрос: а как выбрать, какие методы мы будем оборачивать, а какие — нет? Мы же не хотим, чтобы наш timing-замер внезапно обернул половину инфраструктуры, а заодно печать баннеров на старте приложения. Вот тут и появляется 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("took " + (System.nanoTime() - start) + " ns");
}
}
}
Да, ProceedingJoinPoint — слово длинное. Но смысл довольно дружелюбный: это объект, через который advice может сказать: «а теперь выполни тот метод, который мы оборачиваем». Именно proceed() — это “пусковой крючок” выполнения target-метода.
Полезно запомнить два практических факта сразу. Если вы не вызовете proceed(), целевой метод не выполнится вообще (и вы будете долго смотреть на пустую консоль, думая, что сервис сломан). Если вы не вернёте результат proceed() для методов с return value, вы сломаете контракт метода и получите неожиданные null или ошибки типов. То есть around-advice — это не “декорация”, это реальная часть цепочки вызова.
6. Aspect: pointcut + advice
Теперь соберём понятия в один класс. Aspect — это место, где живут pointcut’ы и advice, объединённые общей идеей. Например, “timing сервисов” — это одна идея. “техническое логирование вызовов” — другая. “проверка прав” — третья. Важно, что аспект — это не «что-то вне 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 аспект не участвует в жизни контейнера, а значит не может влиять на вызовы.
Есть тонкий момент, который очень полезно держать в голове уже сейчас: наличие aspect-класса ещё не гарантирует, что proxy начнут применяться именно “как AOP”. Для этого в чистом Spring нужно включить AOP-инфраструктуру; иначе класс с @Aspect так и останется обычным bean-ом с дополнительной аннотацией. Но даже до этого шага важно понимать: аспект — это не магический файл конфигурации, а класс в вашем проекте, который живёт по правилам DI, scopes и 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 matches?
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 для timing-aspect’а:
import org.springframework.stereotype.Service;
@Service
public class OrderPlacementService {
public void placeOrder() {
// Пример целевого метода сервиса: он будет обёрнут proxy, если попадёт под pointcut.
System.out.println("placing order"); // выводим в консоль факт размещения заказа
}
}
А вот доменная модель — не кандидат на AOP в нашем курсе (и это даже хорошо). Order, OrderItem, команды и события — это обычные объекты, они создаются как часть бизнес-логики, часто через new, и вообще не обязаны быть bean’ами. AOP в нашем сценарии не про “всё подряд”, а про управляемые контейнером сервисы.
И ещё один важный момент: не путайте техническую обвязку и бизнес-аудит. Аудит у нас — часть доменного сценария (он может писаться через AuditWriter, он публикуется через события и listeners). Timing и техническое логирование — это то, что мы будем выносить в 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 не сработает. Это особенно часто всплывает в учебных демо: «я создал сервис вручную и вызвал метод — почему timing не вывелся?». Потому что вы обошли контейнер, и это буквально то же самое, что пытаться пройти в метро не через турникет.
Ошибка №5: тащить бизнес-логику в aspect «раз уж он всё равно всё перехватывает».
Аспект — место для технической обвязки. Как только вы начинаете решать в нём бизнес-вопросы (например, «если заказ дорогой — делай то-то»), вы делаете поведение приложения неявным. Сервисы перестают быть читаемыми, а сценарий начинает зависеть от «скрытого слоя». На первых шагах это может казаться удобно, но дальше обычно превращается в поддержку «а почему оно так решило?».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ