AOP на этапе компиляции без динамических прокси¶
2 августа 2026
Аспектно-ориентированное программирование имеет проблему репутации в экосистеме Java. Лежащая в основе идея полезна: некоторое поведение действительно принадлежит вокруг вызова метода, а не внутри самого бизнес-метода. Транзакции, валидация, повторы, circuit breakers, кеширование, трассировка, авторизация, метрики и логирование — всё это подходит под это описание. Проблема не в идее перехвата. Проблема в том, как перехват традиционно реализован.
Во многих Java-фреймворках аннотация, которая выглядит безобидной в исходном коде, может активировать удивительно большой механизм времени выполнения. Контейнер обнаруживает аннотацию, решает, что объект нуждается в перехвате, создаёт прокси, хранит метаданные, описывающие цепочку советов, разрешает перехватчики и направляет вызовы методов через этот механизм. Приложение может скомпилироваться, даже если часть этой модели недействительна, потому что конечный граф объектов и стратегия перехвата разрешаются только при запуске приложения. Аннотация выглядит декларативной и простой, тогда как путь выполнения становится косвенным.
Фреймворк Kora выбирает другой подход. Его модель AOP построена вокруг генерации на этапе компиляции. Вместо того чтобы ждать выполнения и обнаруживать, что компоненту требуется перехват, Kora обрабатывает соответствующие аннотации во время компиляции и генерирует конкретный класс, содержащий логику перехвата. Для Java это происходит через обработку аннотаций; для Kotlin — через KSP. Сгенерированный исходный код — это обычный исходный код Java или Kotlin. Он становится частью графа приложения, как и другие сгенерированные компоненты Kora, и JVM выполняет его как обычный скомпилированный код.
Это различие звучит как деталь реализации, пока мы не проследим его через всю инженерную модель. Перенос AOP с выполнения на этап компиляции меняет то, когда обнаруживаются ошибки, какой код работает в проде, как разработчик отлаживает перехваченный метод, как JVM оптимизирует путь вызова, как ведёт себя запуск, сколько метаданных нужно хранить и даже как следует понимать классическую проблему самовызова.
Поэтому важное сравнение — не просто:
а:
Оба подхода могут обеспечить декларативный перехват. Оба могут заставить аннотацию запускать поведение вокруг вызова метода. Оба могут реализовать одни и те же сквозные задачи. Но они размещают сложность в очень разных частях системы. Kora намеренно платит большую часть этой сложности во время компиляции, чтобы меньше оставалось в приложении времени выполнения.
AOP — это на самом деле про преобразование пути вызова¶
Прежде чем сравнивать реализации, полезно убрать часть исторической терминологии и посмотреть на механическую проблему. Предположим, приложение содержит сервис:
Теперь представьте, что методу нужны валидация, трассировка и поведение повтора. Концептуально вызов больше не должен означать только:
Он должен означать что-то ближе к:
вызывающий
|
v
проверить входные данные
|
v
начать трассировку
|
v
политика повтора
|
v
PaymentService.pay()
|
v
завершить трассировку
Бизнес-разработчик мог бы написать всё это вручную, но тогда политика инфраструктуры была бы вперемешку в каждом методе сервиса. AOP существует, чтобы отделить эти ортогональные задачи от бизнес-логики. Центральный инженерный вопрос — не в том, полезен ли перехват; он в том, когда и как фреймворк превращает декларативную аннотацию в исполняемый путь вызова.
Фреймворк с прокси времени выполнения отвечает: во время выполнения, вставляя промежуточный объект и направляя вызовы через механизм перехватчиков.
Kora отвечает: на этапе компиляции, генерируя промежуточный класс как исходный код.
Этот выбор — основа всего остального в этой статье.
Традиционная модель прокси времени выполнения¶
Прокси времени выполнения — это объект, создаваемый при запуске или во время работы приложения. Фреймворк обычно изучает некоторую комбинацию метаданных класса, аннотаций, интерфейсов, определений бинов и конфигурации, а затем решает, что вызовы к компоненту должны перехватываться.
Упрощённая архитектура выглядит так:
Прокси обычно не пишется разработчиком приложения. В зависимости от фреймворка и конфигурации он может быть реализован с помощью JDK-динамического прокси, байткода, сгенерированного во время выполнения, подкласса, созданного такой библиотекой, как Byte Buddy или CGLIB, или другого механизма вызова. Точная технология варьируется, но обязанности времени выполнения схожи: определить, какие методы снабжены советами, создать или настроить прокси, связать перехватчики с каждым вызовом, сохранить достаточно метаданных для корректного выполнения совета и убедиться, что внедрение зависимостей раскрывает прокси там, где ожидается перехват.
Обобщённый API вызова может концептуально выглядеть так:
Это чрезвычайно гибко. Фреймворк может принимать решения, используя информацию времени выполнения, и может применять общую инфраструктуру перехвата ко многим классам без генерации специфичного для приложения исходного кода заранее. Обратная сторона в том, что модель выполнения остаётся динамической. Обычно выглядящий вызов метода может войти в обобщённый обработчик вызова, изучить метаданные, выбрать перехватчики, выделить состояние вызова и в конце концов достичь цели.
Современные JVM очень хороши в оптимизации кода, и хорошо спроектированные фреймворки прокси могут сделать многие из этих путей быстрыми. Было бы неправильно сводить сравнение к устаревшему
утверждению, что все прокси времени выполнения медленны, потому что используют Method.invoke. Более важное различие архитектурное: проксирование времени выполнения держит часть механизма принятия
решений фреймворка живой в производственном процессе.
Kora спрашивает, должна ли эта механика оставаться динамической, когда соответствующая структура уже доступна во время компиляции.
Модель AOP на этапе компиляции в Kora¶
Kora переносит решение о перехвате в свой конвейер обработки аннотаций. Компилятор видит компонент, видит AOP-аннотации, проверяет, можно ли перехватить метод, разрешает требуемую поддержку аспекта и генерирует конкретный класс вокруг исходного компонента.
Концептуально сгенерированный класс может выглядеть так:
public final class $PaymentService__AopProxy
extends PaymentService {
private final PaymentValidator validator;
private final Retry paymentRetry;
private final Tracer tracer;
public $PaymentService__AopProxy(
PaymentValidator validator,
Retry paymentRetry,
Tracer tracer
) {
this.validator = validator;
this.paymentRetry = paymentRetry;
this.tracer = tracer;
}
@Override
public PaymentReceipt pay(Payment payment) {
validator.validate(payment);
var span = tracer.startSpan("payment");
try {
return paymentRetry.execute(
() -> super.pay(payment)
);
} finally {
span.end();
}
}
}
class `$PaymentService__AopProxy`(
private val validator: PaymentValidator,
private val paymentRetry: Retry,
private val tracer: Tracer
) : PaymentService() {
override fun pay(payment: Payment): PaymentReceipt {
validator.validate(payment)
val span = tracer.startSpan("payment")
try {
return paymentRetry.execute {
super.pay(payment)
}
} finally {
span.end()
}
}
}
Это концептуальный пример, а не обещание, что каждый аспект Kora порождает ровно такую форму исходного кода. Важно то, что нет нужды в обобщённом обработчике вызова времени выполнения, который обнаруживал бы, что должно произойти для этого метода. Компилятор уже произвёл тело метода, выражающее требуемое поведение.
Во время выполнения путь гораздо ближе к:
вызывающий
|
v
$PaymentService__AopProxy.pay()
|
+--> валидация
|
+--> трассировка
|
+--> повтор
|
v
super.pay()
Сгенерированный класс всё ещё можно обоснованно называть прокси, потому что он стоит между вызывающим и исходной реализацией метода. Сгенерированные классы Kora сами используют AOP-прокси-именование. Решающее различие в том, что этот прокси не обнаруживается и не конструируется как поздний механизм времени выполнения. Он генерируется до запуска приложения, компилируется вместе с приложением, встраивается в граф приложения и поставляется как часть артефакта развёртывания.
Время выполнения исполняет предсгенерированный код, а не интерпретирует модель AOP.
Прокси времени выполнения против сгенерированного подкласса¶
Архитектурный контраст можно резюмировать так:
| Аспект | Прокси времени выполнения | Сгенерированный подкласс/обёртка Kora |
|---|---|---|
| Обнаружение | Обычно запуск/старт контейнера | Этап компиляции |
| План перехвата | Строится или разрешается динамически | Материализуется в сгенерированном исходном коде |
| Неверная форма метода | Может обнаруживаться поздно | Может отклоняться во время компиляции |
| Метаданные выполнения | Обычно требуются | Нужно меньше общих метаданных выполнения |
| Путь вызова | Обобщённый механизм прокси/перехватчика | Сгенерированное тело метода |
| Рефлексия | Обычна в инфраструктуре выполнения | Kora стремится избегать рефлексии выполнения |
| Работа при запуске | Может происходить обнаружение/конструирование прокси | Код прокси уже существует |
| Отладка | Часто пересекает общие внутренности фреймворка | Сгенерированный класс можно открыть и прочитать |
| Поверхность оптимизации | Обобщённая/динамическая | Специализированный байткод для каждого метода |
| Самовызов | Часто проблематичен для внешних пар прокси/цель | Следует семантике виртуальной диспетчеризации подкласса |
| Статический анализ | Больше поведения выполнения для реконструкции | Больше конечной структуры существует в артефакте |
Таблица полезна, но более глубокое следствие в том, что Kora меняет место, где живёт абстракция. Фреймворк AOP времени выполнения держит абстракцию как абстракцию выполнения. Kora использует абстракцию в написанном вручную исходном коде, а затем опускает её в явный код реализации во время компиляции.
Этот шаблон встречается по всей Kora, не только в AOP. Связывание зависимостей, репозитории, HTTP-обработчики, мапперы и другая инфраструктура следуют той же философии компиляции. Поэтому AOP — не изолированная оптимизация. Это одно из выражений общей архитектуры фреймворка.
Перехват метода как сгенерированный код¶
Перехват метода — это основной механизм, стоящий за AOP. Если мы поймём этот механизм в Kora, большинство остальных различий следуют естественно.
Представьте сервис с аннотацией повтора:
Фреймворк AOP времени выполнения может фактически выполнять что-то вроде:
вызвать прокси
|
v
найти метаданные метода
|
v
найти перехватчик повтора
|
v
создать контекст вызова
|
v
retryInterceptor.invoke(context)
|
v
context.proceed()
|
v
target.loadProduct(id)
Генератор этапа компиляции может вместо этого породить выделенное переопределение:
Высокоуровневое поведение одинаково. Модель выполнения — нет. Нет фундаментальной нужды в объекте, представляющем «текущий вызов метода», если компилятор может выразить операцию как обычный код. Нет нужды искать политику повтора из общих метаданных метода, если у сгенерированного класса уже есть правильная зависимость. Нет нужды обнаруживать, какой целевой метод должен выполняться следующим, если сгенерированный метод может вызвать его напрямую.
Сгенерированный метод становится цепочкой перехватчиков.
Это одна из самых важных идей AOP на этапе компиляции: цепочка не обязана оставаться структурой данных во время выполнения; она может стать потоком управления.
Вместо представления:
и интерпретации этого списка, генератор может опустить цепочку во вложенный код:
Точная форма зависит от аспектов, но механически именно это делает опускание на этапе компиляции. Разница напоминает интерпретацию дерева выражений против компиляции его в исполняемый поток управления. Оба могут представлять одну и ту же семантику; один сохраняет обобщённую модель во время выполнения, другой заранее производит специализированный код.
Разработчики Java часто связывают аннотации с последовательностью:
Эта ассоциация историческая, а не фундаментальная. Аннотации также могут быть входными данными для генерации кода на этапе компиляции. Kora рассматривает их в первую очередь как видимые компилятору объявления, из которых можно генерировать исходный код.
Аннотация, активирующая валидацию, кеширование, отказоустойчивость, транзакции, безопасность или логирование, говорит процессору, что методу нужно дополнительное поведение. Сгенерированный класс содержит код, необходимый для присоединения этого поведения. Работающему приложению не нужно заново обнаруживать аннотацию и спрашивать, что она означает, при каждом запуске процесса.
Это проводит важное различие между двумя видами декларативного программирования:
и:
Первое просит разработчиков доверять механизму фреймворка, который материализуется позже. Второе даёт им артефакт реализации, который можно изучить до запуска приложения.
Kora по-прежнему предоставляет декларативный API. Разработчики не пишут вручную каждую обёртку повтора, транзакции или валидации. Но у объявления есть видимый продукт компиляции.
Реальные методы часто имеют более одной сквозной задачи. Операция сервиса может одновременно валидироваться, кешироваться, повторяться, трассироваться, логироваться и быть транзакционной. Как только задействовано несколько аспектов, порядок становится семантически важным.
Рассмотрим:
Эти структуры не эквивалентны:
и:
Аналогично, транзакция вне цикла повтора ведёт себя иначе, чем транзакция, создаваемая для каждой попытки повтора. Валидация перед поиском в кеше может вести себя иначе, чем валидация только при промахах кеша. Трассировка вокруг всей операции отличается от трассировки каждой попытки отдельно.
Модель перехватчика времени выполнения обычно представляет композицию как цепочку объектов, каждый из которых вызывает что-то вроде proceed(). AOP на этапе компиляции может разрешить ту же
композицию при генерации класса. Генератор может определить порядок и материализовать его как вложенный поток управления.
Концептуально:
Поэтому сгенерированный исходный код — это больше, чем артефакт реализации. Это также диагностическое представление композиции аспектов. Когда разработчики спрашивают: «Какой аспект снаружи какого?», есть конкретное место, куда смотреть.
Это важно, потому что ошибки порядка аспектов — одни из самых трудных проблем AOP для диагностики, когда эффективная цепочка собирается косвенно из конфигурации, приоритетов, правил контейнера времени выполнения и метаданных прокси. Генерация на этапе компиляции не может устранить семантическую сложность, но может сделать конечный результат проверяемым.
Проверка на этапе компиляции меняет модель сбоев¶
Сильнейшее преимущество AOP на этапе компиляции — не сырая скорость. Это перенос целого класса сбоев раньше в жизненный цикл разработки.
Рассмотрим этапы, на которых может быть обнаружена ошибка:
редактор
->
компиляция
->
запуск теста
->
интеграционный тест
->
развёртывание
->
первый производственный запрос
Чем позже появляется ошибка, тем она дороже. Фреймворк времени выполнения может принять исходный код, который синтаксически и по типам корректен, даже если запрошенное поведение AOP фактически не может быть применено задуманным образом. Сбой может затем возникнуть, когда контейнер создаёт бин, когда фабрика прокси пытается сделать подкласс класса, когда перехватчик разрешает зависимости или только когда вызывается конкретный метод.
AOP на этапе компиляции имеет доступ к структуре программы, пока компиляция уже идёт. Это позволяет отклонять неподдерживаемые комбинации до создания артефакта приложения.
Основанный на подклассах AOP Kora накладывает конкретные структурные требования. Java-класс, который должен быть подклассифицирован, не может быть final. В Kotlin, где классы и методы по умолчанию
final, перехватываемый класс и соответствующие методы должны быть open, чтобы были возможны сгенерированные переопределения. Это не случайный синтаксис; это прямое следствие модели реализации.
Если генератору нужно произвести форму вида:
то final Service или final operation() не могут участвовать в этой модели. Kora раскрывает ограничение, а не прячет его за трюками с байткодом времени выполнения. Компилятор может затем объяснить
проблему, пока разработчик ещё редактирует код.
Это принципиально лучшее время для сбоя.
Предположим, Kotlin-сервис написан так:
Классы и методы Kotlin final, если не объявлено иначе. Сгенерированный подкласс не может переопределить load. Исправленный класс делает требование перехвата явным:
Это ограничение можно описать как недостаток AOP на основе подклассов, и это реальный компромисс. Но оно также делает механизм честным. Класс open, потому что фреймворк сгенерирует подкласс. Метод open, потому что фреймворк его переопределит. Исходный код отражает семантику диспетчеризации, требуемую фреймворком.
Фреймворки времени выполнения иногда могут ослаблять подобные ограничения через инструментацию, вейвинг или другие техники байткода, но каждый дополнительный механизм увеличивает дистанцию между семантикой исходного кода и поведением выполнения. Kora выбирает более узкую модель, которую легче обдумывать и проверять.
Проверка на этапе компиляции не ограничивается проверкой того, можно ли технически переопределить метод. Аспект может проверять собственный контракт. Он может требовать непустое имя политики, поддерживаемый тип возвращаемого значения, параметры, которые могут образовывать ключ кеша, поддерживаемую форму метода или зависимости, которые можно разрешить из графа приложения.
Фреймворк времени выполнения тоже может проверять эти условия, но должен делать это позже. Процессор этапа компиляции может проверить всё, что уже известно из исходного кода, пока у него есть модель типов.
Это меняет роль компилятора. В обычном Java-приложении компиляция проверяет языковые правила и статические типы. В фреймворке компиляции компилятор также становится этапом архитектурной валидации. Сборка может отвечать на такие вопросы:
Можно ли проксировать этот класс?
Можно ли переопределить этот метод?
Поддерживается ли эта аннотация здесь?
Можно ли разрешить зависимости аспекта?
Можно ли сконструировать сгенерированный класс?
Может ли граф приложения содержать сгенерированный компонент?
Сгенерированный исходный код затем проходит обычную компиляцию Java или Kotlin как ещё один слой корректности. Kora не просто порождает непрозрачные метаданные. Она порождает исходный код, который должен удовлетворять тому же языковому компилятору, что и написанный вручную код.
Это даёт фреймворку мощное свойство: сгенерированный AOP-код проверяется по типам как обычный код приложения.
Граф приложения и AOP связаны¶
AOP не существует вне внедрения зависимостей. Сгенерированным прокси нужны зависимости. Аспект валидации может требовать объекты валидатора. Аспект повтора нуждается в инфраструктуре отказоустойчивости. Транзакционный аспект нуждается в транзакционных или базовых компонентах. Кеш-аспект нуждается в компонентах кеша. Аспекты логирования и телеметрии требуют своих коллабораторов.
С прокси времени выполнения эти связи могут быть спрятаны внутри механики контейнера. Генерация на этапе компиляции может сделать их обычными конструкторными зависимостями.
Концептуально:
public final class $OrderService__AopProxy
extends OrderService {
private final OrderValidator validator;
private final TransactionManager txManager;
private final Retry retry;
public $OrderService__AopProxy(
OrderRepository repository,
OrderValidator validator,
TransactionManager txManager,
Retry retry
) {
super(repository);
this.validator = validator;
this.txManager = txManager;
this.retry = retry;
}
}
Поэтому AOP-прокси — часть графа приложения, а не загадочная оболочка, добавляемая после разрешения зависимостей. Если аспект требует инфраструктуру, эта инфраструктура может участвовать в тех же правилах графа и жизненного цикла, что и другие компоненты.
Граф, который генерирует Kora, не просто конструирует цель, а затем спрашивает вторую фазу выполнения, следует ли проксировать эту цель. Сама сгенерированная проксированная форма может быть компонентом, который получают вызывающие.
Это снижает концептуальный раскол между определением бина, экземпляром цели, фабрикой прокси, цепочкой перехватчиков и раскрываемым объектом.
Есть две связанные стратегии перехвата на этапе компиляции, которые стоит различать. Сгенерированный подкласс использует наследование:
Сгенерированная обёртка использует композицию:
Обёртка может выглядеть так:
Подкласс выглядит так:
На высоком архитектурном уровне оба — статически сгенерированные альтернативы конструированию прокси времени выполнения. Однако для самовызова семантика подкласса имеет огромное значение. Модель
сгенерированного AOP Kora документирована в терминах сгенерированных подклассов прокси, поэтому правила Java final и Kotlin open важны и почему внутренние виртуальные вызовы ведут себя иначе, чем
классическая модель обёртка/цель.
Самовызов: классическая проблема прокси времени выполнения¶
Самовызов — одна из самых известных ловушек прокси-основанного AOP. Рассмотрим этот сервис:
Теперь представьте традиционный внешний прокси:
Вызов от другого компонента идёт через прокси:
Но когда processInvoice() выполняется на цели и вызывает this.charge(invoice), вызов происходит внутри целевого объекта. Он не покидает цель, не выходит наружу к внешнему прокси и не возвращается
обратно.
Путь такой:
а не:
В результате аннотации на внутренне вызываемом методе могут быть обойдены. Это классическая проблема самовызова, связанная с внешним прокси, который оборачивает отдельный экземпляр цели.
Разработчики часто обходят её, вынося метод в другой бин, внедряя прокси-ссылку на себя, спрашивая контейнер о проксированной версии себя или переключаясь на вейвинг. Эти обходные пути существуют,
потому что прокси — не тот объект, чьи внутренние вызовы this выполняются.
Почему перехват сгенерированным подклассом меняет самовызов¶
Перехват на основе подкласса имеет другую семантику диспетчеризации. Рассмотрим исходный класс:
Теперь сгенерируем:
Предположим, фактический объект в графе приложения — экземпляр $BillingService__AopProxy. Когда вызывающий вызывает service.processInvoice(invoice), подкласс наследует processInvoice() от
BillingService. Внутри этого унаследованного метода вызов charge(invoice) — виртуальный вызов на this. Но this — это экземпляр сгенерированного подкласса.
Поэтому обычная виртуальная диспетчеризация JVM ищет наиболее специфичное переопределение charge(), находит сгенерированное переопределение прокси и входит в логику повтора.
Путь становится:
экземпляр $BillingService__AopProxy
|
v
BillingService.processInvoice()
|
v
виртуальный вызов: this.charge()
|
v
$BillingService__AopProxy.charge()
|
v
повтор
|
v
super.charge()
Это важное отличие от классической схемы «прокси содержит цель». Сгенерированный подкласс — не просто внешний объект, окружающий отдельную цель для обычной диспетчеризации. Он и есть объект выполнения.
Поскольку обычная виртуальная диспетчеризация остаётся в силе, многие вызовы того же экземпляра к переопределяемым методам естественно достигают сгенерированных переопределений. Поэтому категоричное утверждение «AOP-прокси не могут перехватывать самовызов» слишком широко. Оно описывает распространённую архитектуру внешнего прокси, а не все механизмы прокси.
Пределы самовызова¶
Генерация подкласса на этапе компиляции не делает каждый внутренний вызов перехватываемым. Обычные правила диспетчеризации Java и Kotlin по-прежнему действуют.
Приватный метод нельзя переопределить:
Final-метод нельзя переопределить:
Статический метод — не виртуальная диспетчеризация экземпляра:
Явный вызов super.method() намеренно обходит переопределение. Конструкторы также имеют особую семантику и не могут перехватываться как обычные переопределяемые методы.
Поэтому правильная ментальная модель — не:
а:
Если внутренний вызов метода виртуален и сгенерированный подкласс переопределяет этот метод, обычная диспетчеризация может достичь аспекта. Если вызов не может участвовать в переопределении, перехват на основе подкласса не может встроиться в этот вызов.
Это одно из сильнейших свойств прозрачности модели: те же языковые правила, которые разработчики уже знают, продолжают иметь значение.
Проблема самовызова проявляется в транзакциях, кешировании, повторах, валидации, безопасности и многих других практических случаях.
Рассмотрим транзакции:
С внешним целевым прокси вызов importOrders(), вызывающий saveOrder() внутренне, может обойти транзакционный перехватчик. С перехватом на основе подкласса, если saveOrder() переопределяем и
фактический экземпляр — сгенерированный подкласс, обычная виртуальная диспетчеризация может направить внутренний вызов через сгенерированное транзакционное переопределение.
Те же рассуждения применимы к повторам:
и кешированию:
О поведении можно рассуждать, используя обычную диспетчеризацию, вместо запоминания специфичного для фреймворка правила, что аннотации перестают работать, когда соседний метод их вызывает.
Это не значит, что каждый внутренний метод должен становиться границей AOP. Чёткие границы компонентов всё ещё легче поддерживать. Важно то, что механизм согласован с объектной моделью.
Использование super в сгенерированных переопределениях стоит понимать точно. Когда переопределение сгенерированного прокси завершает применение аспектов, оно должно в конце концов достичь исходной
реализации. Вызов:
делает ровно это. Он намеренно обходит текущее переопределение, чтобы метод не рекурсировал бесконечно.
Поэтому два релевантных пути:
Внешний вызов¶
Внутренний виртуальный вызов из исходного кода к другому перехваченному методу¶
исходный метод, выполняющийся на экземпляре прокси
->
this.otherMethod
->
переопределение proxy.otherMethod
->
другой аспект
->
super.otherMethod
Это обычная семантика наследования. Не требуется специального поиска самопрокси. Понимание виртуальной диспетчеризации и super объясняет большую часть AOP на основе подкласса без какого-либо
фольклора фреймворка.
Производительность — это вопрос всей системы¶
Обсуждения производительности AOP часто схлопываются в микробенчмарки одного вызова прокси. Это слишком узко. Релевантный бюджет производительности включает как минимум четыре этапа:
Kora намеренно переносит работу со второго и третьего этапов к первому. На этапе сборки процессоры изучают структуру исходного кода и генерируют классы. При запуске приложению не нужно выполнять тот же уровень рефлексивного обнаружения или планирования прокси времени выполнения. В установившемся состоянии перехваченные методы выполняют конкретный сгенерированный байткод.
Это не значит, что аспект имеет нулевые накладные расходы. Политика повтора проверяет состояние и может зацикливаться. Кеш выполняет поиск. Валидация выполняет валидаторы. Транзакция начинает или присоединяется к транзакционному состоянию. Трассировка создаёт или обновляет контекст телеметрии. Эти затраты реальны, потому что реально само поведение.
Значимое утверждение уже: Kora минимизирует накладные расходы фреймворка вокруг поведения, которое аспекту действительно нужно выполнить.
Фреймворк не может убрать существенную стоимость транзакции базы данных, но может избежать наслоения обобщённого механизма вызова времени выполнения поверх неё.
Обобщённый механизм перехватчика времени выполнения часто нуждается в некоторой комбинации объекта вызова, дескриптора метода, массива аргументов, состояния позиции цепочки, поиска метаданных, косвенного вызова цели и адаптации исключений. Сложный фреймворк может кешировать или оптимизировать большую часть этого, и HotSpot может инлайнить стабильные пути. Проксирование времени выполнения не автоматически медленно.
Но обобщённая механика существует, потому что выполнение должно сохранять гибкость.
Если компилятор уже знает:
метод = UserService.load(long)
аспекты = валидация -> кеш -> повтор
зависимости = validator, userCache, retry
целевой вызов = super.load(id)
то производственный процесс не нуждается в обобщённом представлении этих фактов. Генератор может породить точную последовательность.
Это может привести к меньшему числу поисков метаданных, меньшему числу объектов фреймворка на горячем пути, меньшему числу точек динамической диспетчеризации, меньшему числу временных выделений и более лёгкой специализации JIT. Степень выгоды зависит от нагрузки, но архитектурное направление простое: убрать гибкость, которая работающему приложению больше не нужна.
Конкретный байткод дружелюбен к JIT¶
HotSpot отлично оптимизирует конкретный код. Рассмотрим сгенерированный метод:
Байткод соответствует обычному доступу к полям, обычным вызовам и обычному потоку управления. JIT может профилировать эти места вызовов, как любой другой код приложения.
Сильно обобщённый движок AOP времени выполнения может вводить общие абстракции вызова через множество несвязанных методов. Эти пути тоже можно оптимизировать, но они представляют более обобщённую структуру. Сгенерированный код имеет естественное преимущество специализации: вместо одного механизма, поддерживающего каждый бин, каждый метод и каждую комбинацию перехватчиков, есть небольшой сгенерированный метод, поддерживающий именно этот метод с именно этими аспектами.
Та же причина делает генерацию кода привлекательной для сериализаторов, мапперов баз данных, HTTP-маршрутизации и внедрения зависимостей. Если структура известна заранее, специализация может превратить общую проблему в прямолинейный код.
Kora применяет этот принцип последовательно.
Обобщённый перехват времени выполнения может создавать короткоживущие объекты, особенно если каждый вызов представлен объектом, содержащим метод, аргументы, цель и состояние продолжения. Современные сборщики мусора очень хорошо справляются с короткоживущими выделениями, поэтому это не следует преувеличивать, но устранение избегаемых выделений всё ещё полезно на высокопроизводительных путях.
Сгенерированный перехват может держать аргументы метода как обычные локальные переменные:
вместо концептуального стирания их в универсальное представление:
Не каждый фреймворк времени выполнения буквально выделяет именно эти объекты, но архитектурное различие остаётся. У генератора этапа компиляции нет требования стирать типизированный вызов в универсальную модель вызова. Он может сохранить исходную сигнатуру через слой перехвата.
Типизированная прямота помогает производительности и отлаживаемости одновременно.
Фреймворк может иметь очень быстрый установившийся вызов прокси и всё равно налагать стоимость в другом месте. Обнаружение и конструирование прокси происходят при запуске. Рефлексивные метаданные могут увеличивать работу инициализации. Классы, сгенерированные во время выполнения, нужно создавать и загружать. Кеши метаданных перехватчиков потребляют память. Обобщённая инфраструктура увеличивает поверхность прогрева.
AOP на этапе компиляции в Kora снижает объём AOP-специфичного принятия решений, выполняемого живым процессом. Это согласуется с более широкой философией запуска фреймворка: подготовить структуру приложения до начала продакшена.
Для облачных сервисов запуск — не только удобство разработчика. Экземпляры запускаются во время развёртываний, автоскейлинга, замены узлов, восстановления spot-инстансов, интеграционных тестов, локальной разработки, аварийного восстановления и рабочих процессов scale-from-zero. Даже небольшие кусочки обнаружения времени выполнения умножаются по флоту.
AOP на этапе компиляции — одна из частей снижения этой повторяющейся стоимости.
Было бы вводяще в заблуждение утверждать, что AOP на этапе компиляции автоматически делает каждый сервис Kora быстрее любого сервиса с прокси времени выполнения. Реальная производительность зависит от задержки базы данных, сериализации, сетевых вызовов, пулов соединений, логирования, телеметрии, ограничений конкурентности, поведения GC и многих других факторов.
Более сильный аргумент архитектурный. AOP на этапе компиляции убирает категории работы времени выполнения, которым не нужно оставаться динамическими. Это снижает нижнюю границу накладных расходов фреймворка. Если бизнес-операция занимает миллисекунды, разница может быть невидимой. Если метод чрезвычайно дёшев и вызывается миллионы раз, накладные расходы перехвата могут иметь значение. Если критичен запуск, важно устранение планирования прокси времени выполнения. Если память ограничена, важно снижение метаданных и обобщённой инфраструктуры.
Вот почему дизайн AOP Kora следует понимать как часть бюджета производительности, а не как историю одного бенчмарка вызова.
Отлаживаемость: открой сгенерированный класс¶
Одно из сильнейших практических свойств дизайна AOP Kora — то, что разработчики могут изучать сгенерированный исходный код. Документация Kora явно поощряет смотреть на сгенерированные классы AOP-прокси для таких функций, как валидация, кеширование и отказоустойчивость. Эта рекомендация раскрывает важную философию фреймворка: сгенерированный код не рассматривается как неловкая деталь реализации, которую пользователи никогда не должны видеть. Он часть поверхности отладки.
Если аннотация повтора ведёт себя неожиданно, разработчик может изучить сгенерированное переопределение. Если инвалидация кеша происходит не в том месте, сгенерированный метод показывает, где она выполняется. Если несколько аннотаций отказоустойчивости странно взаимодействуют, сгенерированный прокси раскрывает их вложенность. Если внедрённая зависимость выглядит удивительно, сгенерированный конструктор может показать, что прокси фактически требует.
Сгенерированный код действует как исполняемое объяснение того, что фреймворк понял из аннотаций.
Это большое отличие от модели прокси времени выполнения, где эффективный путь выполнения может существовать только как комбинация метаданных времени выполнения, типа прокси, регистрации перехватчиков, правил порядка и разрешения цели.
Точка останова на сервисе с прокси времени выполнения может войти в сгенерированный класс, который не является частью дерева исходного кода, а затем пройти через обобщённую инфраструктуру фреймворка:
обработчик вызова прокси
перехватчик метода фреймворка
цепочка перехватчиков
абстракция вызова метода
рефлексия / method handle
фактическая цель
Современные IDE справляются с такими стеками, и зрелые фреймворки предоставляют хорошую инструментацию. Проблема не в том, что отладка невозможна. Проблема в том, что понимание одного перехваченного метода может потребовать понимания обобщённого движка времени выполнения фреймворка.
Вопрос отладки становится:
Что фреймворк делает с моим методом во время выполнения?
Со сгенерированным исходным кодом вопрос другой:
Какой код фреймворк сгенерировал для моего метода?
Это значительный сдвиг. Первое требует реконструкции динамического поведения. Второе даёт разработчику конкретный артефакт.
Сгенерированный код сужает семантический разрыв¶
Семантический разрыв — это дистанция между тем, что исходный код, кажется, означает, и тем, что выполнение фактически делает. Обычный Java-вызов имеет относительно небольшой семантический разрыв:
AOP-фреймворк неизбежно добавляет поведение, которое не написано внутри load(). В сильно динамической модели реальный путь может быть:
service.load(id)
-> прокси
-> перехватчик безопасности
-> транзакционный перехватчик
-> перехватчик метрик
-> перехватчик повтора
-> target.load(id)
Генерация на этапе компиляции не может устранить семантический разрыв, потому что декларативный AOP намеренно добавляет поведение. Что она может сделать — материализовать недостающий слой как исходный код.
Концептуально:
Теперь есть мост между аннотацией и поведением выполнения. Бизнес-класс остаётся кратким, тогда как сгенерированный класс объясняет фактический путь выполнения.
Это сильный компромисс между декларативной эргономикой и операционной прозрачностью.
Сгенерированный AOP не гарантирует красивые стек-трейсы. Аспекты добавляют дополнительные фреймы, потому что дополнительный код реально выполняется. Компоненты повтора, трассировки, кеширования, валидации и транзакций появляются в стеке, потому что они часть пути вызова.
Важное различие в том, что фреймы сгенерированного прокси могут сопоставляться с реальными сгенерированными классами и методами. Стек может концептуально содержать:
Это легче связать с кодом приложения, чем обобщённую последовательность обработчиков вызова, связь которых с перехваченным методом нужно реконструировать. Если сгенерированные исходники доступны IDE, разработчики могут проходить по этим методам, как по обычному коду.
То же помогает при анализе производственных стек-трейсов постфактум. Имя сгенерированного класса сообщает, что задействован слой AOP, а имя метода идентифицирует перехваченную операцию.
Прозрачность не означает, что фреймворк исчезает. Она означает, что фреймворк оставляет понятные свидетельства того, что он делает.
Те же свойства, которые помогают человеческой отладке, помогают статическому анализу и нейро-инструментам кодирования. Динамический фреймворк может требовать, чтобы инструмент выводил поведение сразу из нескольких слоёв:
- аннотаций исходного кода;
- соглашений фреймворка;
- правил прокси времени выполнения;
- порядка перехватчиков;
- конфигурации контейнера;
- сгенерированного байткода выполнения;
- реестров метаданных.
Фреймворк компиляции раскрывает гораздо больше этого поведения как исходный код. Инструмент, исследующий проблему, может изучить бизнес-метод, сгенерированный прокси, сгенерированный граф приложения и прямые зависимости, используемые аспектом. Он может следовать по фактическому пути кода, вместо того чтобы угадывать, какая механика выполнения будет собрана.
Это важно в больших кодовых базах, где автоматизированные инструменты всё чаще выполняют рефакторинг, диагностику, ревью кода, генерацию тестов и анализ инцидентов. О статических и явных структурах легче рассуждать, чем о поведении, существующем только после запуска приложения.
Магия времени выполнения против механики этапа компиляции¶
Заманчиво резюмировать генерацию кода на этапе компиляции как «никакой магии». Это отражает опыт разработчика, но технически неточно. Обработка аннотаций — это механика. KSP — это механика. Разрешение аспектов — это механика. Генерация исходного кода — это механика. Генерация графа — это механика.
Значимое различие:
Kora по-прежнему выполняет сложную работу фреймворка. Разница в том, что большая часть результата становится исходным кодом до запуска приложения. Реализация фреймворка может быть сложной, тогда как сгенерированная модель выполнения остаётся простой.
Сложность не исчезла. Она перенесена в фазу, где может выполниться один раз, произвести диагностику, сгенерировать типизированный код и оставить после себя меньший производственный рантайм.
Некоторые разработчики, услышав «сгенерированный подкласс», заключают, что это не настоящий AOP, а только генерация кода. Это различие не полезно. AOP — это про модуляризацию сквозных задач и применение поведения в определённых точках соединения. Выполнение метода — здесь точка соединения. Аннотации определяют, где применяются аспекты. Сгенерированные переопределения вплетают поведение вокруг этих выполнений методов.
Происходит ли вплетение во время генерации исходного кода, компиляции, загрузки классов, запуска контейнера или вызова во время выполнения — меняет стратегию реализации, а не архитектурную цель.
Модель Kora корректно описывается как AOP на этапе компиляции, потому что применение аспекта определяется и материализуется, пока программа строится, а не динамически собирается во время обычного выполнения приложения.
AOP на этапе компиляции против вплетения байткода¶
Сгенерированные подклассы также отличаются от вплетения байткода на этапе компиляции. Вплетатель байткода может модифицировать сам исходный класс. Концептуально:
становится:
Это может перехватывать случаи, которые подклассирование не может, потому что изменяется само тело исходного метода. В зависимости от технологии вплетения, оно может затрагивать приватные методы, конструкторы, доступ к полям и другие точки соединения.
Обратная сторона в том, что класс больше не ведёт себя ровно так, как предполагает файл исходного кода, если разработчик не понимает процесс вплетения. Отладка и инструменты сборки могут усложняться, потому что байткод преобразуется после обычной компиляции исходного кода.
Генерация подкласса в Kora уже. Исходный класс не переписывается. Отдельный сгенерированный класс расширяет его и переопределяет перехватываемые методы. Это ограничение напрямую способствует прозрачности.
Фреймворк отказывается от части теоретической мощности перехвата в обмен на модель, которую легче изучать и объяснять.
Java-агенты могут инструментировать классы во время загрузки или переопределять их, пока приложение работает. Это мощные инструменты для профайлеров, платформ наблюдаемости, отладки и некоторых AOP-систем. Но агент вводит ещё один этап преобразования времени выполнения:
Для инфраструктуры, которая действительно должна присоединяться к произвольным приложениям без изменений исходного кода, это отличная возможность. Для задач, принадлежащих фреймворку, чья структура уже известна на этапе сборки, это часто ненужная сложность.
Kora не нуждается в агенте времени выполнения только для применения стандартных аспектов фреймворка к управляемым методам приложения. Это снижает число движущихся частей между исходным кодом и исполняемым кодом.
Повторяющийся вопрос прост:
Если ответ можно знать на этапе компиляции, зачем откладывать его до выполнения?
Цена отказа от динамики времени выполнения¶
AOP на этапе компиляции имеет компромиссы. Фреймворк прокси времени выполнения может принимать решения, используя информацию, которой не существует во время компиляции. Он может присоединять перехватчики на основе обнаружения плагинов времени выполнения, динамической регистрации, среды развёртывания или компонентов, загружаемых после сборки основного артефакта приложения.
Генерация на этапе компиляции предпочитает более закрытое представление структуры приложения. Это идеально для большинства бэкенд-сервисов, где код и аннотации определяют модель компонентов до развёртывания. Это менее естественно для систем, чьё основное требование — расширяемость во время выполнения.
Предположим, приложение загружает произвольные сторонние плагины из каталога и применяет вновь обнаруженные перехватчики к типам, которых не существовало во время основной сборки. Проксирование или инструментация времени выполнения могут лучше подходить для этой проблемы.
Kora оптимизирует под другую среду:
известный код сервиса
известные контракты фреймворка
известные аннотации
известный граф приложения
известный артефакт развёртывания
Для обычных микросервисов и бэкенд-приложений это обычно фактическая операционная модель.
Большинство сервисов не изобретают новые классы бизнес-сервисов после развёртывания. Их контроллеры известны. Репозитории известны. Границы транзакций известны. Аннотации повтора и кеша известны. Аннотации валидации известны. Артефакт собирается в CI и развёртывается неизменяемо.
Эта среда сильно благоприятствует решениям на этапе компиляции. Если сервис переразвёртывается всякий раз, когда меняется его код, требование пересборки для изменения структуры AOP — не значимое ограничение. Единица развёртывания уже является скомпилированным приложением.
Поэтому Kora согласует архитектуру фреймворка с операционной реальностью современных неизменяемых развёртываний. Сервис — не универсальный контейнер времени выполнения. Это конкретная программа, и конкретная программа выигрывает от конкретного сгенерированного кода.
Разработчики Java часто предпочитают final-классы, потому что finality сообщает, что наследование не является частью публичного дизайна класса. Kotlin идёт дальше и делает классы final по умолчанию. AOP на основе подкласса создаёт напряжение с этим принципом, потому что перехватываемые компоненты должны допускать наследование, порождаемое фреймворком.
Это реальный компромисс. Компонент open не потому, что разработчики приложения должны вручную его подклассифицировать, а потому, что генерация кода использует наследование как механизм реализации.
Альтернативные механизмы перехвата имеют свои затраты. Прокси подклассов времени выполнения всё равно могут требовать не-final-методы. Инструментация переписывает поведение невидимо. Прокси интерфейсов требуют границ, ориентированных на интерфейсы. Обёртки вводят отдельные экземпляры делегата и могут менять поведение самовызова.
Ни одна реализация AOP не бесплатна. Выбор Kora имеет преимущество: ограничение явное, статически проверяемое и легко объяснимое.
Естественный вопрос: почему AOP на этапе компиляции не всегда избегает наследования и не генерирует обёртки. Например:
Это хорошо работает, когда приложение полностью спроектировано вокруг интерфейсов. Но у обёрток есть компромиссы. Они не автоматически сохраняют API конкретного класса. Они вводят отдельный экземпляр делегата. Внутренние вызовы происходят на делегате и не возвращаются через обёртку, воссоздавая классическую проблему самовызова. Внедрение по конкретному типу может стать менее естественным, и каждый раскрытый метод должен быть переадресован.
Подклассирование сохраняет больше исходной объектной модели. Что важнее всего для самовызова, унаследованные методы выполняются на том же экземпляре сгенерированного подкласса, который содержит переопределения перехвата.
Это даёт AOP на основе подкласса особенно когерентную модель диспетчеризации.
AOP на основе подкласса на этапе компиляции не делает интерфейсы нерелевантными. Интерфейсы остаются ценными архитектурными границами для сервисов, клиентов, репозиториев и тестирования. Они отделяют контракты от реализаций и снижают связанность.
Что следует концептуально разделять:
и:
Сервис может раскрывать интерфейс, пока Kora всё ещё генерирует конкретную реализацию или класс прокси на этапе компиляции. Аналогично, интерфейс репозитория может действовать как контракт генерации кода на этапе компиляции, а не реализовываться через обработчик вызова времени выполнения.
Это повторяющийся шаблон Kora: сохранять знакомые Java-абстракции, избегая ненужной интерпретации этих абстракций во время выполнения.
Пример: валидация¶
Валидация — полезный пример, потому что семантика прямолинейна.
Бизнес-код:
Концептуальный сгенерированный путь:
Слой AOP знает параметры метода статически. Он может генерировать типизированные вызовы. Если аннотация применена к неподдерживаемой форме, сборка может провалиться. Если требуемую зависимость валидатора нельзя разрешить, сгенерированный граф может раскрыть это во время компиляции.
Когда валидация падает во время выполнения, она падает потому, что входные данные нарушают правило валидации, а не потому, что фреймворк слишком поздно обнаружил, что не может сконструировать механизм валидации.
Это чисто отделяет структурные ошибки фреймворка от фактических ошибок бизнес-входа.
Пример: кеширование¶
Кеширование демонстрирует, как AOP на этапе компиляции может кодировать логику ветвления.
Бизнес-метод:
Сгенерированное поведение концептуально напоминает:
Реальный код кеша должен учитывать обнуляемость, композицию ключей, семантику бэкенда, телеметрию и конкурентность, но структурный момент тот же: специфичный для метода поток кеша может быть порождён напрямую.
Теперь рассмотрим инвалидацию:
Сгенерированный метод может точно выразить, происходит ли инвалидация до или после super.updateUser(), происходит ли она только при успехе и какой кеш/ключ используется. Эти детали видны в коде, а
не спрятаны в обобщённом состоянии перехватчика.
Пример: повтор и circuit breaker¶
Аспекты отказоустойчивости сложнее, потому что поток управления сложнее. Повтор может включать счётчики попыток, backoff, классификацию исключений, задержки, отмену и метрики. Circuit breaker имеет переходы состояний, разрешения, запись успехов/сбоев, полуоткрытое поведение и, возможно, fallback.
Бизнес-метод не должен содержать всё это:
AOP на этапе компиляции может генерировать клей, соединяющий метод с переиспользуемыми компонентами отказоустойчивости:
Сами компоненты отказоустойчивости остаются объектами времени выполнения, потому что их состояние действительно динамическое. Текущее состояние circuit breaker нельзя скомпилировать прочь. Решения о повторе зависят от фактических сбоев. Таймауты зависят от реального времени.
AOP на этапе компиляции не переносит состояние выполнения в компилятор. Он переносит статическую структуру перехвата в компилятор.
Это разделение фундаментально:
Этап компиляции:
- какой метод перехватывается
- какой аспект применяется
- как упорядочены аспекты
- какие зависимости их реализуют
- можно ли переопределить метод
- как прокси встраивается в граф
Время выполнения:
- содержимое кеша
- состояние circuit breaker
- исход попытки повтора
- текущая транзакция
- текущий трейс/span
- входные данные валидации
- принципал безопасности
Компилятору не нужно знать, провалится ли следующий удалённый вызов. Ему нужно только знать, какой компонент повтора должен получить вызов. Ему не нужно знать, будет ли присутствовать ключ кеша. Ему нужно только сгенерировать путь поиска.
Kora — не «всё на этапе компиляции». Это этап компиляции для вещей, которые действительно можно решить на этапе компиляции.
Пример: транзакции¶
Транзакции — архетипичная задача AOP, потому что они окружают бизнес-операцию.
Концептуально сгенерированный метод можно понимать так:
Важно не точное API транзакций. Важно то, что граница транзакции становится конкретным вызовом в сгенерированном коде.
При отладке проблемы транзакции сгенерированное переопределение может отвечать на прямые вопросы: где начинается транзакция, какой менеджер используется, какой исходный вызов метода находится внутри границы и сидит ли другой аспект внутри или снаружи транзакции.
Декларативная аннотация транзакции ценна, потому что шаблонный код повторяющийся. AOP на этапе компиляции сохраняет удобство без требования, чтобы граница транзакции существовала только как метаданные прокси времени выполнения.
Порядок аспектов становится архитектурой¶
Как только применяется несколько аспектов, порядок перестаёт быть деталью реализации фреймворка. Это архитектура приложения.
Рассмотрим:
Две возможные структуры:
и:
Они могут давать очень разную семантику базы данных. Подобные вопросы возникают для таймаута и повтора, кеша и транзакции, трассировки и повтора, валидации и кеширования, или circuit breaker и fallback.
Хорошая AOP-система должна определять детерминированный порядок. Система этапа компиляции получает дополнительное преимущество: как только порядок разрешён, сгенерированный исходный код раскрывает ответ. Поэтому сгенерированный код становится способом аудита композиции аспектов.
Обновления фреймворка часто трудны, потому что аннотации остаются неизменными, пока поведение под ними меняется. Представьте метод, который остаётся:
Если порядок аспектов фреймворка меняется, бизнес-исходник может не показать никакого диффа. Сгенерированный исходный код всё ещё может раскрыть изменение от:
к:
Командам не нужно коммитить все сгенерированные исходники в систему контроля версий, чтобы получить от этого выгоду. Сгенерированный вывод можно изучать во время миграций, захватывать в артефактах CI или сравнивать в сфокусированных регрессионных тестах.
Важно то, что конечная структура перехвата существует как текст, который можно сравнивать.
Тестирование сырого класса против тестирования сгенерированного компонента¶
AOP усложняет тестирование всякий раз, когда модульный тест напрямую конструирует сервис:
Этот экземпляр — сырой класс, а не сгенерированный AOP-подкласс. Если тест ожидает поведение повтора, валидации, кеширования, транзакции или безопасности, которое Kora применяет через AOP, ручное конструирование обходит слой аспектов.
Это не уникально для Kora. Фреймворки прокси времени выполнения имеют то же различие между тестированием сырой цели и тестированием управляемого/проксированного компонента.
Сгенерированная модель Kora делает два слоя концептуально ясными:
UserService
= бизнес-реализация
$UserService__AopProxy
= бизнес-реализация + сгенерированные аспекты
Чистый модульный тест может намеренно упражнять первое. Компонентный или интеграционный тест может намеренно упражнять второе через граф приложения Kora.
Это различие помогает командам писать тесты, которые явно указывают, что они проверяют, вместо случайной зависимости от поведения контейнера.
Предположим, рефакторинг делает перехватываемый Java-класс final или убирает open из перехватываемого Kotlin-метода. В модели времени выполнения проблема может появиться только когда тест запускает
соответствующий контекст приложения. Если ни один тест не создаёт эту конфигурацию, проблема может уйти ещё дальше.
В модели этапа компиляции процессор видит, что требуемый подкласс или переопределение не может быть сгенерирован. Сборка падает до упаковки.
Это мощно, потому что компиляция обычно самый ранний универсальный этап в CI. Даже команды с несовершенным покрытием интеграционных тестов обычно компилируют каждый изменённый модуль. Поэтому AOP на этапе компиляции превращает структурную корректность фреймворка в то, что обеспечивается самым фундаментальным шагом сборки.
Это снижает зависимость от «повезло ли нам запустить тест, который стартует этот компонент?».
Экономика прямолинейна. Ошибку компиляции обычно сразу видит разработчик, который её внёс. Ошибку запуска может увидеть тестовый раннер, система CI, staging-развёртывание или операционная команда. Ошибку производственного поведения могут увидеть клиенты.
Каждый более поздний этап добавляет прошедшее время, инфраструктуру, логи, людей, неопределённость и стоимость отката. Поэтому перенос ошибки с выполнения на компиляцию имеет мультипликативную ценность.
Вот почему проверку на этапе компиляции следует рассматривать как функцию надёжности, а не только как эргономику разработчика. Дизайн AOP Kora создаёт возможности перенести структурное неправильное использование в самый дешёвый возможный домен сбоя.
Когда ошибки фреймворка появляются быстро и точно, экспериментирование становится дешевле. Разработчик может добавить аннотацию, скомпилировать, прочитать диагностику, исправить форму метода и продолжить. Если та же ошибка требует упаковки, запуска всего приложения, ожидания контейнера и интерпретации стек-трейса конструирования прокси, цикл обратной связи длиннее и когнитивно дороже.
Бэкенд-разработка состоит из сотен таких циклов. Малые задержки накапливаются.
Фреймворки компиляции используют компилятор как интерактивного партнёра по дизайну. Компилятор не только проверяет синтаксис Java; он проверяет, может ли фреймворк фактически построить запрошенную структуру приложения.
AOP — одно из мест, где эта обратная связь особенно ценна, потому что ограничения прокси иначе легко обнаружить поздно.
Стоимость этапа сборки реальна¶
Перенос работы на этап компиляции не заставляет работу исчезнуть. Процессоры аннотаций потребляют CPU. Обработка KSP потребляет CPU. Сгенерированный исходный код нужно компилировать. Крупные приложения могут генерировать много файлов. Дизайн процессора может влиять на инкрементальные сборки.
Компромисс можно описать так:
Стоит ли этот компромисс того, зависит от того, насколько хорошо спроектирована генерация. Для бэкенд-сервисов часто стоит. Сборка CI происходит один раз за артефакт. Затем артефакт может запускаться десятки, сотни или тысячи раз по репликам и средам, а его пути выполнения могут выполняться миллиарды раз.
Конвейер сборки также более безопасное место для траты CPU, чем чувствительный к задержке производственный процесс.
Фреймворк компиляции не может пренебрегать производительностью сборки. Продуктивность разработчика сильно зависит от инкрементальной компиляции. Если изменение одного метода регенерирует неоправданно большую часть приложения, цикл редактирования может стать неприятным, даже если производственная производительность отличная.
Поэтому хорошая инженерия фреймворка компиляции требует детерминированной генерации, разумного отслеживания зависимостей, эффективного кеширования сборки и ограниченной инвалидации, где возможно. Подход Kora к этапу компиляции следует оценивать не только по запуску и пропускной способности, но и по поведению чистой и кешированной сборки.
Стоимость фреймворка не исчезла. Она переместилась, и новое местоположение всё ещё нужно оптимизировать.
Сгенерированное переопределение сохраняет исходную сигнатуру метода:
Компилятор сохраняет полную информацию о типах. Сгенерированный код не может случайно передать String туда, где ожидается long. Возвращаемые значения остаются типизированными. Обобщённые границы
должны компилироваться. Проверяемые исключения должны подчиняться правилам Java. Зависимости аспектов могут раскрывать типизированные API.
Сравните это с универсальной формой вызова:
Такой API намеренно стирает специфичные для метода типы, чтобы представлять каждый вызов. Код фреймворка затем должен реконструировать смысл через метаданные, приведения, соглашения или сгенерированные помощники.
Генерация на этапе компиляции может дольше оставаться в типизированном мире. Это не только свойство производительности. Это делегирует больше проверки корректности языковому компилятору.
Обобщённый API прокси часто рассматривает параметры как Object[]. Это представление гибко, но теряет структуру. Примитивы могут требовать упаковки. Идентичность параметра переходит в метаданные.
Выполнение видит обобщённую коллекцию вместо отдельных строго типизированных локальных переменных.
Сгенерированный код может сохранить:
как именно эти переменные.
Это зеркалит сгенерированные сериализаторы и мапперы, которые обращаются к полям напрямую, вместо рефлексии над ними во время выполнения. Когда структура статична, сохранение этой структуры обычно лучше, чем её стирание и повторное обнаружение позже.
Сквозное поведение иногда описывается так, будто это окружающая магия. В реальности каждый аспект зависит от конкретных вещей.
Кеширование зависит от кеша. Валидация зависит от валидаторов. Транзакции зависят от транзакционной инфраструктуры. Отказоустойчивость зависит от объектов политики и состояния. Телеметрия зависит от метров, трейсеров и логгеров. Безопасность зависит от состояния аутентификации и авторизации.
AOP на этапе компиляции поощряет авторов фреймворков и разработчиков приложений думать об этом как об обычных зависимостях. Аннотация транзакции означает, что сгенерированный код вызовет транзакционный компонент. Аннотация повтора означает, что сгенерированный код вызовет инфраструктуру отказоустойчивости. Как только эти связи представлены как поля, параметры конструктора и вызовы методов, об AOP становится гораздо легче рассуждать.
Граф приложения может затем проверять и связывать эти зависимости, как любые другие.
Что разработчику следует изучать¶
При отладке AOP Kora особенно полезны четыре слоя.
Во-первых, изучите написанный вручную бизнес-класс. Подтвердите аннотацию, сигнатуру метода, видимость и то, можно ли переопределить класс/метод.
Во-вторых, изучите сгенерированный класс $...__AopProxy и найдите переопределение метода. Это раскрывает фактическую сгенерированную цепочку аспектов.
В-третьих, проследите поля или параметры конструктора, используемые этим сгенерированным методом. Они идентифицируют конкретные задействованные компоненты повтора, кеша, валидации, транзакции, безопасности или телеметрии.
В-четвёртых, если кажется, что подключён не тот компонент, изучите сгенерированный граф приложения, чтобы увидеть, как сам прокси конструируется и внедряется.
Этот путь замечательно конкретен:
бизнес-метод
->
сгенерированное переопределение
->
зависимость аспекта
->
сгенерированное связывание графа
Он превращает отладку фреймворка в навигацию по коду.
Одно из самых трудных свойств больших фреймворков времени выполнения — нелокальные рассуждения. Метод ведёт себя иначе из-за глобального реестра, файла конфигурации, сканирования classpath, режима прокси, автоконфигурации или правила порядка, определённого в другом месте.
Сгенерированный AOP Kora снижает часть этой дистанции. Чтобы понять один перехваченный метод, разработчик часто может посмотреть на метод, его аннотации, сгенерированное переопределение и его прямые зависимости.
Это локальные рассуждения.
У всего фреймворка по-прежнему есть конфигурация и общая инфраструктура, но критический путь перехвата материализован рядом как код. Локальные рассуждения лучше масштабируются по командам, потому что инженеры могут понять одну границу сервиса без реконструкции всего состояния контейнера времени выполнения.
Ошибки фреймворка случаются. Генератор может неправильно обработать обобщённую сигнатуру, распространение аннотаций, Kotlin-переопределение, комбинацию аспектов или форму метода с крайними случаями. Когда падающее поведение представлено в сгенерированном исходном коде, отчёт об ошибке может включать этот исходный код.
Минимальный репро-кейс может показать:
Это часто легче диагностировать, чем ошибку, зависящую от состояния фабрики прокси времени выполнения, поведения загрузчика классов или метаданных, собранных только после запуска.
Сгенерированный код превращает часть внутреннего решения фреймворка в воспроизводимый текстовый артефакт. Это помогает сопровождающим фреймворка так же, как разработчикам приложений.
Как только фреймворк поощряет пользователей изучать сгенерированный код, читаемость становится неформальным контрактом. Именование переменных имеет значение. Структура метода имеет значение. Именование сгенерированных классов имеет значение. Ненужная косвенность становится видимой.
Это создаёт здоровое давление. Если сгенерированный AOP-код нелепо сложен, пользователи замечают. Если сгенерированный путь выделяет ненужные объекты, инженеры производительности могут это изучить. Если порядок аспектов удивителен, вложенность видна.
Поэтому прозрачность действует как механизм качества. Сгенерированный код — не просто вывод компилятора. Это свидетельство того, что решил фреймворк.
Если сгенерированный AOP-код читаем и довольно прямолинеен, разумный вопрос: почему бы не написать его вручную?
Потому что ценность генерации ровно в том, что людям не нужно поддерживать повторяющуюся инфраструктуру. Сервис с двадцатью методами и комбинациями валидации, логирования, трассировки, кеширования, транзакций и повторов накопил бы большие объёмы кода обёрток. Сигнатуры методов нужно было бы синхронизировать вручную. Порядок аспектов мог бы дрейфовать. Телеметрия могла бы забываться. Обработка ошибок могла бы стать несогласованной.
Генерация даёт приложению простоту явных обёрток во время выполнения без наложения стоимости поддержки написанных вручную обёрток.
Задуманное разделение:
разработчик пишет:
краткое декларативное намерение
компилятор создаёт:
явную повторяющуюся реализацию
выполнение исполняет:
обычный код
Люди сохраняют абстракцию. JVM получает специализированную реализацию.
Хорошая сгенерированная инфраструктура должна быть скучной. Она должна выглядеть как то, что компетентный разработчик мог бы написать вручную:
Скучной код ценен, потому что предсказуем. Он не требует ментальной модели интерпретатора мини-языка. Он не прячет поток управления в графе метаданных выполнения. Он не заставляет разработчиков учить вторую модель выполнения наряду с Java.
Поэтому акцент Kora на человекочитаемом сгенерированном исходном коде не косметический. Читаемость — это ограничение дизайна архитектуры фреймворка.
AOP на этапе компиляции и нативные образы¶
Статическая, слаборефлексивная инфраструктура в целом лучше подходит для компиляции ahead-of-time, чем сильно динамическая механика выполнения. Системам native-image нужно понимать, какие классы, методы, конструкторы, ресурсы и рефлексивные обращения остаются достижимыми. Динамические прокси и рефлексия могут требовать дополнительных метаданных, чтобы сборщик образов не удалял код, нужный позже.
Сгенерированные AOP-классы — обычные классы. Их конструкторные зависимости явны. Их вызовы методов видны в байткоде. Их связь с графом приложения статична.
Это делает их легче для понимания статическим анализом. Это не значит, что каждое приложение Kora становится тривиальным для нативной компиляции — библиотеки всё ещё могут использовать рефлексию, ресурсы, нативный код или другое динамическое поведение — но AOP на этапе компиляции убирает один распространённый источник динамики времени выполнения.
Даже на HotSpot та же статическая структура выгодна для запуска и прозрачности.
Безопасность — не главная причина для AOP на этапе компиляции, но более ранняя валидация может снизить опасную неправильную конфигурацию. Если аннотация безопасности требует аспект и аспект не может быть сгенерирован, компиляция может провалиться. Если форма перехватываемого метода неподдерживаема, фреймворк может отклонить её, а не молча работать без задуманного совета.
Это важно, потому что самый опасный сбой фреймворка — не видимое исключение. Это метод, который выглядит защищённым, потому что присутствует аннотация, тогда как фактический путь выполнения обходит контроль безопасности.
Проверка на этапе компиляции снижает пространство для этого несоответствия. Она не может предотвратить плохие правила авторизации или уязвимости бизнес-логики, но может проверить больше структурного присоединения поведения безопасности до развёртывания.
Для контролей безопасности свойство «отказ при компиляции» привлекательно.
AOP может тонко менять поведение исключений. Перехватчик может оборачивать, переводить, повторять, подавлять, записывать или запускать fallback для исключения. Рефлексивный вызов исторически также
вводил обёртки, такие как InvocationTargetException, требуя от кода фреймворка распаковки реального сбоя.
Сгенерированные прямые вызовы могут избегать части случайной механики исключений, потому что super.method() — обычный вызов. Повтор и fallback всё ещё намеренно меняют поток управления, но
сгенерированный исходный код может показать, где это происходит.
Концептуально:
Разработчик может рассуждать из обычной семантики исключений Java вместо трассировки через несколько обобщённых рефлексивных обёрток.
Профайлеры группируют CPU-сэмплы по методам и классам. Стабильные сгенерированные классы упрощают атрибуцию. Горячий метод, такой как:
немедленно сообщает, что AOP участвует в пути. Flame graph может концептуально выглядеть так:
HTTP-обработчик
UserController
$OrderService__AopProxy.place
TransactionManager
Retry
OrderService.place
Repository
Фрейм сгенерированного прокси несёт смысл приложения. Обобщённые стеки перехватчиков времени выполнения могут концентрировать сэмплы в общей инфраструктуре, используемой многими несвязанными методами, в зависимости от реализации и JIT-инлайнинга.
Сгенерированная специализация естественно сохраняет больше идентичности приложения в данных профилирования.
Каждая подсистема времени выполнения — ещё одна вещь, которая может выйти из строя после развёртывания. Фабрики динамических прокси могут сталкиваться с крайними случаями загрузчиков классов. Генерация классов времени выполнения может падать. Рефлексия может взаимодействовать с границами модулей. Метаданные прокси могут быть несогласованными. Поиск перехватчика может выбрать не тот путь.
Генерируя и компилируя прокси до развёртывания, Kora снижает объём AOP-специфичной механики, которая должна успешно работать в проде. У процессора всё ещё могут быть ошибки, но эти ошибки с большей вероятностью проявятся во время сборки, генерации кода или тестов.
Полезный операционный принцип:
AOP на этапе компиляции воплощает этот принцип.
Система модулей Java усилила инкапсуляцию вокруг рефлексивного доступа. Фреймворки, сильно зависящие от рефлексии, иногда требуют директив opens, чтобы обращаться к членам во время выполнения.
Сгенерированный код подкласса работает через обычные правила доступа и наследования Java. Это не устраняет все проблемы системы модулей, но выравнивает их с компиляцией. Если сгенерированный исходный код не может легально обратиться к типу или члену, компилятор может сказать об этом.
Это предпочтительнее обнаружения нелегальной рефлексивной проблемы после того, как приложение уже запустилось.
Перехват на этапе компиляции не предотвращает конфигурацию времени выполнения. Сгенерированный прокси может зависеть от компонента повтора, чьи попытки и задержка приходят из конфигурации. Сгенерированный кеш-аспект может использовать кеш, чей размер или срок действия различаются между средами. Транзакционный аспект может использовать пулы соединений и настройки базы данных времени выполнения.
Этап сборки может решить:
тогда как конфигурация времени выполнения решает:
Генерация на этапе компиляции фиксирует топологию, а не каждое операционное значение. Этот баланс важен: статическая структура, динамическая настройка.
Поскольку Kora помещает поведение AOP в сгенерированный подкласс, а не переписывает каждое написанное вручную тело метода, исходный класс остаётся узнаваемым как бизнес-код. Модульный тест может инстанцировать его напрямую. Разработчик может изучать доменную логику без чтения инфраструктурной обвязки фреймворка. Инструмент миграции иногда может переиспользовать его без слоя аспектов, если это намеренно.
Это разделение ценно:
Оба читаемы. Ни один не притворяется другим.
Обратная сторона в том, что написанный вручную класс — не вся история выполнения, когда присутствуют аннотации. Сгенерированный подкласс завершает эту историю.
Предположим, сервис, обычно управляемый Kora, имеет AOP-аннотации. Если производственный код вручную делает:
то этот экземпляр — сырой класс, а не сгенерированный подкласс прокси. Его методы выполняются без сгенерированного поведения AOP.
Это правило прямолинейно:
Экземпляр графа Kora:
сгенерированный подкласс -> аспекты активны
Ручной сырой экземпляр:
базовый класс -> аспекты отсутствуют
Для модульных тестов ручное конструирование может быть намеренным. Для производственного кода компоненты, полагающиеся на поведение фреймворка, обычно следует конструировать через граф приложения.
AOP принадлежит чётким границам компонентов¶
Семантика подкласса на этапе компиляции делает внутренний перехват более когерентным, чем классические внешние прокси, но это не значит, что каждый вспомогательный метод должен становиться точкой соединения AOP. О сквозных задачах легче всего рассуждать на значимых границах компонентов.
Например:
Граница транзакции или отказоустойчивости на операции сервиса понятна. Повтор вокруг вызова внешнего клиента понятен. Кеш вокруг сервиса чтения понятен. Десятки крошечных аннотированных внутренних помощников всё ещё могут создавать фрагментированную модель управления, даже если перехват технически работает.
AOP на этапе компиляции улучшает механизм. Хорошая архитектура всё ещё определяет, где механизм принадлежит.
Не каждая задача принадлежит аспекту. AOP сильнее всего для поведения, которое действительно ортогонально и переиспользуемо: транзакции, стандартная валидация, политики отказоустойчивости, кеширование, проверки авторизации, телеметрия и стандартизированное логирование.
Бизнес-рабочие процессы, чей порядок определяет доменную семантику, обычно должны оставаться явными. Например:
обычно яснее как обычный код оркестрации, чем как четыре пользовательские аннотации, чей порядок определяет бизнес-поведение.
Генерация на этапе компиляции делает AOP безопаснее и прозрачнее, но не меняет фундаментальное правило дизайна: используй AOP, чтобы убрать инфраструктурный шаблонный код, а не чтобы скрывать домен.
Метод, такой как:
может быть технически валиден, но труден для рассуждений. Даже если сгенерированный код делает результат проверяемым, объявление в исходном коде несёт большую семантическую нагрузку.
AOP на этапе компиляции — не лицензия на максимизацию плотности аннотаций. Аннотации должны представлять стандартизированные политики инфраструктуры с чёткой семантикой. Когда поведение становится уникальным для одного рабочего процесса, обычный код обычно лучше.
Сгенерированная прозрачность помогает определить границу, потому что разработчики могут видеть, сколько поведения фактически присоединено к, казалось бы, маленькому методу.
Если на одном методе может быть несколько аспектов, их правила композиции фактически часть публичного API фреймворка. Разработчикам нужны ответы на такие вопросы:
Выполняется ли валидация до повтора?
Находится ли кеш внутри или снаружи транзакции?
Включает ли трассировка попадания в кеш?
Записывает ли логирование каждую попытку повтора или один логический вызов?
Оборачивает ли таймаут повтор или каждую попытку?
Выполняется ли fallback внутри circuit breaker?
Это не детали реализации. Они меняют производственное поведение.
AOP на этапе компиляции даёт Kora особенно хороший способ сделать композицию детерминированной и проверяемой. Приложения всё равно должны тестировать критические комбинации, тогда как сгенерированный исходный код даёт объяснение уровня реализации, когда что-то ведёт себя неожиданно.
Сгенерированный AOP также улучшает предсказуемость. Динамические системы могут иметь фазовые переходы во время раннего выполнения: инициализация метаданных при первом использовании, создание прокси, разрешение рефлексивных аксессоров, заполнение кеша перехватчиков или генерация классов времени выполнения. Сгенерированный код стартует с более стабильной структурной формы.
Всё ещё есть прогрев JIT. Всё ещё есть ленивые библиотеки. Пулы баз данных всё ещё устанавливают соединения. Кеши всё ещё начинаются холодными. Удалённые системы всё ещё имеют реальную задержку. AOP на этапе компиляции не отменяет прогрев. Он убирает одну категорию специфичного для фреймворка позднего связывания с этой поверхности прогрева.
Для чувствительных к задержке сервисов это может значить больше, чем крошечный выигрыш установившейся пропускной способности. Хвостовая задержка наказывает неожиданную работу инициализации сильнее, чем средняя задержка. Снижение структурного обнаружения времени выполнения делает поведение приложения в ранней жизни легче для рассуждений.
Операционные команды редко заботятся о том, использует ли фреймворк прокси в абстрактном смысле паттерна проектирования. Они заботятся о времени запуска, памяти, CPU, задержке, режимах сбоя, стек-трейсах и диагностируемости.
AOP на этапе компиляции вносит вклад во все эти измерения, убирая работу времени выполнения, где возможно. Эффект одного прокси мал. Эффект по валидации, кешированию, отказоустойчивости, транзакциям, безопасности, логированию и другим задачам фреймворка может быть значимым при умножении на каждый компонент и каждый экземпляр сервиса.
Вот почему архитектуру AOP Kora следует понимать в контексте всего фреймворка. AOP следует тому же шаблону, что и внедрение зависимостей на этапе компиляции, репозитории, маппинги и другая сгенерированная инфраструктура:
Когда многие подсистемы следуют одному правилу, выгоды усиливают друг друга.
Процессоры аннотаций Kora нетривиальны. Сама генерация AOP нетривиальна. Корректная обработка Java и Kotlin, обобщённых сигнатур, распространения аннотаций, нескольких аспектов, видимости методов, зависимостей и сгенерированного исходного кода требует существенной инженерии фреймворка.
Это не противоречит цели более простого выполнения приложения.
Разработчики фреймворков поглощают сложность, чтобы разработчикам приложений и производственным процессам не приходилось. Фреймворк может содержать сложную компиляторную механику, производя прямолинейные сгенерированные классы.
Фреймворк прост не потому, что его собственный исходный код мал. Он прост для пользователей, когда производит предсказуемые программы с понятным поведением. AOP на этапе компиляции намеренно концентрирует сложность в тулчейне, чтобы снизить сложность в развёрнутом сервисе.
Статическая валидация также даёт сопровождающим фреймворка более чистый способ эволюции API. Если комбинация аннотаций становится неподдерживаемой, процессор может отклонить её с целевой ошибкой. Если аспект требует новую зависимость, разрешение графа приложения может раскрыть отсутствующее связывание. Если тип возвращаемого значения метода больше не удовлетворяет контракту аспекта, компиляция может идентифицировать точный метод.
Без проверки на этапе компиляции несовместимое поведение с большей вероятностью проявится как исключение запуска или несоответствие времени выполнения, чья связь с обновлением фреймворка может быть менее очевидной.
Сильная диагностика превращает компилятор в помощника миграции. Это важно в организациях, поддерживающих много сервисов и нуждающихся в том, чтобы обновления фреймворка падали быстро и предсказуемо по большой кодовой базе.
Терминология компилятора даёт точную ментальную модель AOP Kora. Разработчик пишет высокоуровневую конструкцию:
Процессор фреймворка опускает это объявление в более явную реализацию:
Это напоминает опускание в компиляторе. Высокоуровневая конструкция преобразуется в конструкции более низкого уровня с сохранением задуманной семантики. Затем сгенерированная форма проходит обычную компиляцию.
Думая об AOP так, уходит большая часть загадочности. Аннотация — входные данные преобразования. Сгенерированный прокси — опущенное представление. JVM выполняет это представление.
Эта модель также объясняет, почему ошибки компиляции естественны: если фреймворк не может легально опустить объявление в валидный подкласс и переопределение, сборка должна провалиться.
Справедливое сравнение должно признать, что проксирование времени выполнения решало реальные проблемы чрезвычайно хорошо и продолжает это делать. Оно обеспечило декларативные транзакции, безопасность, кеширование и другую инфраструктуру задолго до того, как современные экосистемы генерации кода стали распространёнными. Оно поддерживает приложения с динамическими моделями расширения. Оно может создавать перехват для типов, которые не были доступны при компиляции основного приложения. Зрелые фреймворки прокси также имеют обширную инструментацию и высокооптимизированные реализации.
Аргумент Kora не в том, что проксирование времени выполнения фундаментально недействительно. Он в том, что ограничения обычных современных бэкенд-сервисов позволяют другой компромисс.
Когда структура уже известна до развёртывания, Kora предпочитает компилировать эту структуру в приложение. Проксирование времени выполнения оптимизирует под динамику. Сгенерированный AOP на этапе компиляции оптимизирует под статическую предсказуемость.
Это разные инженерные приоритеты.
Гибкость всегда имеет цену, даже когда цена не в первую очередь CPU. Гибкость времени выполнения требует метаданных, косвенности, поздней валидации, динамического создания типов, правил идентичности прокси, инфраструктуры вызова и большего числа возможных состояний выполнения.
Эти затраты покупают нечто ценное: возможность решать позже.
Поэтому правильный вопрос — не:
Плохи ли прокси времени выполнения?
а:
Действительно ли этому сервису нужно решать позже?
Для большинства компонентов Kora ответ нет. Разработчик уже решил в исходном коде, что метод имеет @Retry, @Cacheable, @Transactional или другую аннотацию аспекта. Сигнатура метода фиксирована.
Класс известен. Граф приложения компилируется. Сервис переразвёртывается, когда эти факты меняются.
Сохранение универсального движка прокси времени выполнения даёт мало выгоды, когда модель развёртывания уже статична.
AOP на этапе компиляции убирает гибкость, которая приложению не нужна.
Холодный старт включает гораздо больше, чем загрузку JVM. Фреймворки могут сканировать classpath, изучать аннотации, создавать определения компонентов, строить прокси, разрешать метаданные перехватчиков и инициализировать внутренние реестры до того, как приложение станет готовым.
Сгенерированный AOP Kora означает, что класс прокси уже присутствует. Сервису не нужно при запуске обнаруживать, что метод аннотирован, а затем решать, как его перехватывать. Граф приложения уже знает тип сгенерированного компонента.
AOP — не единственный фактор запуска, но архитектурная согласованность важна. Если внедрение зависимостей, маппинг, репозитории, HTTP-привязки и AOP все избегают повторного обнаружения времени выполнения, совокупный эффект может быть значительным.
Вот почему AOP на этапе компиляции принадлежит обсуждениям готовности и масштабирования, а не только микробенчмаркам вызовов прокси.
Метаданные времени выполнения занимают память. Фабрики прокси, дескрипторы методов, описания цепочек перехватчиков, рефлексивные кеши и структуры контейнера могут оставаться достижимыми на протяжении жизни приложения. Сгенерированный код тоже потребляет память, когда его классы загружаются, так что нет магического представления с нулевой стоимостью.
Разница в том, что статическую структуру программы часто можно представить более прямо как байткод, чем как обобщённую модель данных времени выполнения. Конкретный сгенерированный метод не нуждается в отдельном объекте, говорящем:
потому что тело метода само кодирует эту последовательность.
По многим компонентам специализация может снизить объём общих метаданных времени выполнения, необходимых для описания той же программы. Точная экономия зависит от реализации и нагрузки, но структурное преимущество ясно.
Современные фреймворки времени выполнения могут использовать MethodHandle, invokedynamic, сгенерированный байткод или другие техники вместо медленного рефлексивного вызова. Это может сделать
вызовы прокси времени выполнения очень эффективными.
Поэтому значимое сравнение — не:
а:
Система времени выполнения, использующая отличную генерацию байткода, всё ещё разрешает некоторую специфичную для приложения структуру после компиляции. Система этапа компиляции разрешает эту структуру до развёртывания.
Этот более сильный аргумент переживает современные реализации прокси и избегает устаревшего фольклора производительности.
Существуют две широкие категории сбоев вокруг перехваченного метода.
Структурные сбои включают:
метод нельзя переопределить
зависимость аспекта отсутствует
использование аннотации недействительно
сгенерированный код не может скомпилироваться
граф приложения несогласован
Операционные сбои включают:
транзакция базы данных падает
удалённый вызов истекает по таймауту
circuit breaker открывается
валидация отклоняет реальный вход
бэкенд кеша становится недоступным
авторизация отказывает в доступе
AOP на этапе компиляции пытается устранить первую категорию до развёртывания. В проде остаются в основном сбои, которые действительно зависят от состояния выполнения.
Это сильное свойство надёжности. Система не притворяется, что неопределённость выполнения можно устранить; она убирает структурную неопределённость, которой никогда не нужно было достигать выполнения.
Во время инцидента инженерам нужны конкретные ответы быстро. Предположим, задержка резко растёт вокруг метода с поведением повтора и таймаута. Эти структуры имеют разную наихудшую задержку:
и:
В первом случае один таймаут может ограничивать всю последовательность повторов. Во втором каждая попытка может получать собственный бюджет таймаута, умножая общую задержку.
Сгенерированный исходный код может показать, какая структура фактически развёрнута. Реагирующие на инцидент могут изучить конкретный прокси вместо реконструкции специфичных для версии правил порядка времени выполнения по памяти.
Под операционным давлением статические свидетельства ценны.
Сгенерированный прокси — обычный тип этапа компиляции. У него детерминированный суперкласс, конкретные поля, конструктор, переопределённые методы и байткод, произведённый обычным компилятором.
Это важно для инструментов. Статические анализаторы могут его изучать. Профайлеры могут о нём сообщать. Стек-трейсы могут его называть. Системы сборки могут кешировать его компиляцию. IDE могут по нему навигировать, когда сгенерированные исходники настроены корректно. Декомпилятор может восстановить его из собранного артефакта.
Прокси, сгенерированные во время выполнения, тоже имеют классы во время выполнения, но эти классы могут не существовать в артефакте развёртывания и их может быть труднее связать обратно с исходным кодом.
Kora намеренно сохраняет эту связь.
С генерацией прокси во время выполнения артефакт может содержать класс приложения и движок фреймворка, тогда как конечный тип прокси появляется только после запуска. Поэтому статическая инспекция JAR не обязательно показывает полную структуру выполнения.
С AOP на этапе компиляции Kora артефакт содержит больше конечного ответа:
класс приложения
сгенерированный AOP-прокси
сгенерированное связывание приложения
реализации аспектов времени выполнения
Это помогает воспроизводимости, отладке, статическому анализу, сканированию безопасности, компиляции ahead-of-time и инспекции производственных артефактов.
Единица развёртывания ближе к фактической программе, которая будет выполняться.
AOP на этапе компиляции становится особенно мощным в паре с внедрением зависимостей на этапе компиляции. Два статических механизма усиливают друг друга:
обработка AOP
->
сгенерированный тип прокси
обработка графа DI
->
сгенерированное конструирование и связывание
Коллабораторы прокси становятся рёбрами графа. Граф можно проверять. Сгенерированный прокси можно конструировать через обычное сгенерированное связывание. Нет нужды во второй фазе выполнения, которая говорит:
Проксированный/сгенерированный тип уже может быть компонентом, который понимает граф.
Это избегает большей части концептуального раскола между целевым объектом, фабрикой прокси, экземпляром прокси, цепочкой перехватчиков и раскрываемым бином.
Системы прокси времени выполнения могут создавать сюрпризы идентичности. getClass() может раскрыть неожиданный сгенерированный тип. Прокси интерфейсов могут быть не приводимы к конкретной
реализации. Проверки равенства или точного класса могут вести себя иначе. Аннотации на методах реализации могут взаимодействовать иначе с проксированием на основе интерфейсов.
Сгенерированный подкласс всё ещё другой конкретный класс, поэтому точные предположения getClass() остаются плохой идеей. Но отношение подтипа естественно:
Объектная модель остаётся близкой к обычному наследованию. Это снижает часть трения, возникающего из-за того, что прокси и цель концептуально отдельные объекты.
Представьте, что разработчик сообщает, что @Retry присутствует, но повтора не происходит. В традиционном фреймворке прокси времени выполнения чек-лист отладки может включать:
Управляется ли объект контейнером?
Был ли создан прокси?
Какая стратегия прокси активна?
Держит ли вызывающий прокси или сырую цель?
Перехватываем ли метод?
Видна ли аннотация там, где прокси её ожидает?
Обходит ли самовызов прокси?
Зарегистрирован ли перехватчик?
Разрешена ли политика?
С AOP на этапе компиляции несколько из этих вопросов смещаются раньше. Если класс нельзя проксировать, компиляция должна провалиться. Если сгенерированный метод существует, разработчик может открыть его и увидеть, присутствует ли логика повтора.
Проблема выполнения становится уже:
Был ли внедрён сгенерированный компонент?
Какой объект повтора был внедрён?
Что этот объект повтора наблюдал во время выполнения?
Более ранняя валидация снижает размерность производственной отладки.
Предположим, запись кеша остаётся после обновления. Сгенерированный прокси может отвечать на конкретные вопросы:
Содержит ли метод логику инвалидации?
Происходит ли инвалидация до или после super.update()?
Какой экземпляр кеша используется?
Какой ключ выводится?
Происходит ли инвалидация только после успеха?
Эти вопросы соответствуют фактическому исходному коду. Точки останова можно ставить в сгенерированном методе. По пути выполнения можно проходить.
Читаемый сгенерированный код превращает внутренности фреймворка в обычные цели отладки. Для большой инженерной команды это может сэкономить больше времени, чем микрооптимизация накладных расходов вызова.
Транзакции особенно чувствительны к самовызову и порядку аспектов. Предположим:
В модели внешней обёртки/цели первый вопрос часто в том, вызывается ли processOne() через транзакционный прокси.
Со сгенерированным подклассом разработчик может рассуждать из правил диспетчеризации. Если batch() выполняется на экземпляре сгенерированного подкласса и вызывает переопределяемый processOne(),
виртуальный вызов может достичь сгенерированного транзакционного переопределения. Если метод приватный или final, он не может.
Правило следует семантике Java/Kotlin, а не внешней модели «прокси-побег и повторный вход».
Диспетчеризация подкласса даёт самовызову более когерентное поведение, но команды всё равно должны тестировать важные границы. Реальный код может включать унаследованные методы, мостовые методы,
обобщения, интерфейсные методы по умолчанию, явные вызовы super, различия видимости и специфичные для языка детали.
Безопасный инженерный принцип — не:
AOP подкласса на этапе компиляции всегда перехватывает каждый внутренний вызов.
а:
Перехват следует за сгенерированным подклассом и обычной семантикой диспетчеризации; важное поведение всё равно должно проверяться тестами.
Это утверждение менее драматично и более точно.
Модель Kotlin «final по умолчанию» делает AOP на основе подкласса невозможным игнорировать. Java-разработчик может забыть, что класс подклассифицируем, потому что Java-классы открыты, если не помечены final. Kotlin-разработчик должен явно написать:
и, где требуется:
Этот дополнительный синтаксис может казаться неудобным, но он также сообщает о механизме. Исходный код говорит читателю, что что-то ожидается как подкласс компонента. В AOP Kora это что-то — сгенерированный код.
Поэтому связь между объявлением в исходном коде и поведением фреймворка необычно явна.
Java продолжает развиваться: более сильная инкапсуляция, модули, records, sealed-типы, виртуальные потоки, лучшие AOT-инструменты, улучшенные JIT и более богатые API компилятора — всё это меняет ландшафт фреймворков. Техники, построенные вокруг обычной семантики языка, обычно хорошо адаптируются, потому что сгенерированный код — это всё ещё просто код.
Если HotSpot узнаёт новую оптимизацию, сгенерированные методы могут выиграть. Если статический анализ улучшается, сгенерированный граф вызовов легче изучать. Если поддержка сгенерированных исходников в IDE улучшается, отладка становится лучше. Если компиляция ahead-of-time становится распространённее, статические классы и вызовы уже хорошо подходят.
Фреймворкам, создающим полностью отдельную модель выполнения времени выполнения, приходится поддерживать эту модель наряду с каждой эволюцией платформы. Генерация на этапе компиляции может больше опираться на экосистему языка и компилятора.
Современные Java и Kotlin поощряют неизменяемость, final-классы и sealed-иерархии. AOP на основе подкласса должен сосуществовать с этими решениями дизайна, а не заставлять каждый класс быть open.
Хорошая архитектура может держать доменные модели final, помещая AOP на границы сервисов, спроектированные для перехвата:
Final доменные/значимые типы
^
|
Открытая управляемая граница сервиса с AOP
^
|
Контроллер / точка входа
Доменные объекты не нуждаются в AOP. Инфраструктурно-ориентированные сервисные компоненты могут намеренно допускать сгенерированное наследование. Это держит механику фреймворка на архитектурных границах, а не распространяет её по всей модели.
Поэтому ограничения этапа компиляции могут поощрять более чёткое наслоение.
Сквозные задачи яснее всего на границах с реальным архитектурным смыслом. Транзакция на методе сервиса говорит: «эта операция атомарна». Повтор вокруг внешнего вызова говорит: «эта интеграция устойчива к временным сбоям». Кеш на операции чтения говорит: «этот результат можно переиспользовать по этой политике».
Эти смыслы легче понять, чем аспекты, разбросанные по низкоуровневым помощникам.
Тот факт, что Kora технически может перехватывать внутренние виртуальные вызовы, не должен поощрять команды превращать каждый метод в границу аспекта. Цель AOP — убрать повторяющуюся инфраструктуру, а не фрагментировать обычный поток управления.
Читаемый сгенерированный код мощный, но не должен становиться оправданием для недокументированного поведения. Разработчикам всё ещё нужна документация для контрактов аннотаций, поддерживаемых форм методов, правил порядка, конфигурации, распространения транзакций, классификации повторов, семантики кеша, правил безопасности и поведения ошибок.
Сгенерированный исходный код отвечает:
Как эта сборка реализовала объявленное поведение?
Документация отвечает:
Какое поведение фреймворк обещает и почему?
Они дополняют друг друга. Сгенерированные прокси Kora наиболее полезны, когда публичный контракт уже ясен, а разработчик хочет изучить конкретную реализацию.
Дизайн фреймворка содержит продуктивное напряжение. Бизнес-код не должен загрязняться повторяющимися инфраструктурными деталями, но инфраструктура не должна становиться настолько скрытой, что инженеры не могут её понять.
AOP на этапе компиляции балансирует эти цели.
Бизнес-код остаётся кратким:
Сгенерированная инфраструктура остаётся видимой, когда нужно:
Разработчики не поддерживают сгенерированный класс, но могут его изучать. Это более сильная модель прозрачности, чем любая из крайностей: писать каждую обёртку вручную или прятать всё внутри движка прокси времени выполнения.
Практическое сравнение бок о бок¶
Рассмотрим один и тот же сервис под двумя концептуальными реализациями.
Прокси времени выполнения¶
Написанный вручную класс:
Путь выполнения:
OrderServiceProxy
|
v
InvocationHandler / механика перехватчика
|
v
RetryInterceptor
|
v
метаданные метода
|
v
цель OrderService
Типичные вопросы отладки включают:
Был ли создан прокси?
Какая стратегия прокси активна?
Какой перехватчик совпал?
Какие метаданные он прочитал?
Вызываю ли я прокси или цель?
Обходит ли самовызов его?
Прокси этапа компиляции Kora¶
Написанный вручную класс:
Сгенерированная форма:
Вопросы отладки становятся более конкретными:
Что породил генератор?
Какой объект Retry был внедрён?
Что этот объект Retry сделал во время выполнения?
Это снижение скрытого состояния и есть практическая ценность AOP на этапе компиляции.
Один из лучших способов понять Kora — перестать относиться к сгенерированным файлам как к временному мусору компилятора. Сгенерированный AOP-прокси — часть программы. Сгенерированный DI-граф — часть программы. Сгенерированные реализации репозиториев и мапперы — часть программы.
Разработчики могут не редактировать эти файлы, но прод их выполняет.
Как только принят такой образ мышления, многие решения дизайна Kora становятся интуитивными. Фреймворк не пытается спрятаться. Он генерирует слой реализации, который более динамичный фреймворк реконструировал бы во время выполнения.
AOP на этапе компиляции вписывается в более широкую философию Kora¶
Общий инженерный паттерн Kora можно резюмировать так:
высокоуровневое объявление
->
проверка на этапе компиляции
->
сгенерированный явный код
->
тонкое выполнение
AOP следует ровно этому пути. Разработчик получает краткие аннотации. Компилятор получает достаточно информации для проверки структуры. Генератор создаёт читаемый исходный код. Выполнение исполняет прямой типизированный код. Приложение избегает ненужной рефлексии и конструирования прокси времени выполнения.
Эта согласованность важна. Фреймворк становится легче учить, когда одна и та же ментальная модель применяется через модули. Как только разработчик понимает сгенерированные репозитории, мапперы или связывание приложения, сгенерированный AOP ощущается как ещё одно использование той же архитектуры компиляции, а не отдельная магическая подсистема.
Поведение прокси времени выполнения становится трудным, когда взаимодействуют несколько осей:
прокси интерфейса
против
прокси класса
внешний вызов
против
самовызов
аннотация на интерфейсе
против
аннотация на реализации
идентичность прокси
против
идентичность цели
упорядоченный перехватчик
против
неупорядоченный перехватчик
условие выполнения истинно
против
условие выполнения ложно
Каждая ось управляема сама по себе. Перекрёстное произведение — нет. Команды в конце концов накапливают фольклор фреймворка, такой как «эта аннотация работает только при вызове из другого бина» или «этот метод должен быть на интерфейсе» или «эта транзакция отсутствует, потому что ты сконструировал класс напрямую».
Генерация на этапе компиляции не может устранить каждое правило, но схлопывает многие из них в один конкретный класс. Если сгенерированное переопределение существует, перехват существует. Если его нельзя сгенерировать, сборка должна объяснить почему.
Это гораздо более сильный инвариант отладки.
Фреймворк мог бы выполнять AOP на этапе компиляции, генерируя байткод напрямую. Это могло бы избежать одного шага компиляции исходного кода, но генерация исходного кода имеет уникальное преимущество прозрачности.
Разработчики могут читать результат без декомпилятора. Навигация IDE может его показывать. Ошибки компилятора могут указывать в него. Документация фреймворка может учить на нём. Инструменты диффа могут его сравнивать. нейро-инструменты могут анализировать его как обычный Java или Kotlin.
Для фреймворка, чьи основные ценности включают прозрачность, генерация исходного кода — больше, чем удобство реализации. Она превращает вывод компилятора в документацию.
Легко переоценить сгенерированный AOP, сказав, что всё становится прямым вызовом. Сгенерированный метод всё ещё может вызывать интерфейсы. Повтор может делегировать абстракции отказоустойчивости. Телеметрия может вызывать API OpenTelemetry. Кеш-код может делегировать Caffeine или Redis. Транзакционный код может вызывать драйвер или абстракцию пула.
Важное свойство в том, что специфичная для приложения топология перехвата явна и сгенерирована. Фреймворку не нужен универсальный диспетчер прокси времени выполнения, чтобы решать, какие аспекты применяются к методу.
Вызовы внутри этого сгенерированного метода затем используют обычные API соответствующих компонентов.
Это согласуется с более широким предпочтением Kora в пользу тонких абстракций, а не построения гигантского специфичного для фреймворка мира выполнения вокруг каждой технологии.
Сквозные задачи содержат и политику, и механизм.
Для повтора:
политика:
какие сбои повторять?
сколько попыток?
какая задержка?
механизм присоединения:
обернуть этот вызов метода выполнением повтора
Для кеширования:
политика:
срок действия, размер, бэкенд
механизм присоединения:
поиск до вызова, сохранение после вызова
Для транзакций:
политика:
изоляция, распространение, поведение соединения
механизм присоединения:
выполнить этот метод внутри границы транзакции
AOP на этапе компиляции в основном генерирует механизм присоединения. Политика может оставаться компонентом, настраиваемым во время выполнения. Это держит сгенерированную структуру статичной, сохраняя операционную настройку.
Сгенерированный исходный код даёт интуитивное визуальное представление вложенности аспектов. Предположим, сгенерированный метод напоминает:
Вложенность немедленно сообщает:
Это часто легче понять, чем чтение нескольких номеров порядка или логов контейнера. Сгенерированный код превращает порядок в обычный поток управления, а разработчики уже умеют читать поток управления.
Статическая генерация хорошо работает с рефакторингом. Предположим, метод меняется от:
к:
Сгенерированный прокси должен быть перегенерирован с новой сигнатурой. Если аспект больше не может поддерживать тип параметра, компиляция может провалиться. Нет устаревших метаданных выполнения, описывающих старую форму метода.
Написанный вручную метод и его сгенерированная инфраструктура пересобираются вместе. Компилятор становится структурным проходом проверки по всем сгенерированным точкам интеграции после каждого рефакторинга.
Это особенно ценно в больших кодовых базах, где сигнатуры методов часто эволюционируют.
В регулируемых или строго контролируемых средах инженерам иногда нужно ответить на точный вопрос:
Какой код фактически выполняется вокруг этого бизнес-метода в этой сборке?
С динамическим AOP ответ может потребовать документирования конфигурации фреймворка и доказательства того, как ведёт себя резолвер времени выполнения. С AOP на этапе компиляции сгенерированный исходный код можно сохранять как часть свидетельств сборки.
Воспроизводимая сборка может сохранять:
исходный код приложения
версию Kora
сгенерированный исходный код
версию компилятора
блокировки зависимостей
конечный артефакт
Сгенерированный прокси фиксирует интерпретацию аннотаций фреймворком для этой сборки. Это не формальная верификация, но конкретный мост между декларативным намерением и исполняемым кодом.
Перехват метода: итоговая оценка¶
Для перехвата метода модель сгенерированного подкласса имеет ясный инженерный профиль. Перехваченный метод сохраняет исходную типизированную сигнатуру. Зависимости аспектов могут становиться обычными
полями. Исходная реализация достигается через обычный вызов super. Несколько аспектов могут становиться вложенным потоком управления. Выполнению не нужно представлять каждый вызов как обобщённую
структуру Method + Object[] + InvocationContext.
Цена столь же ясна: перехват следует обычным правилам подклассирования. Final, приватные, статические или иным образом непереопределяемые методы нельзя перехватывать переопределением подкласса. Модель обменивает часть гибкости на явную семантику уровня языка.
Это разумный компромисс для бэкенд-сервисов, где структура приложения известна во время сборки.
Проверка на этапе компиляции: итоговая оценка¶
Проверка на этапе компиляции меняет AOP из чего-то, что контейнер пытается заставить работать после начала запуска, в то, что сборка доказывает структурно возможным до упаковки.
Неподдерживаемые final-классы или методы можно отклонять. Недействительное использование аннотаций можно отклонять. Отсутствующие зависимости могут всплывать во время конструирования графа. Сгенерированный исходный код сам должен компилироваться. Диагностика может указывать на исходный элемент.
Это снижает поздние сюрпризы и делает корректность фреймворка частью обычного контракта компиляции.
Для большого флота сервисов это преимущество надёжности часто важнее малых различий в задержке вызова прокси.
Самовызов: итоговая оценка¶
Самовызов — место, где сгенерированный AOP подкласса наиболее явно отличается от классической ментальной модели внешнего прокси.
С внешним прокси, оборачивающим отдельную цель, внутренний вызов цели часто никогда не возвращается через прокси. Совет обходится.
Со сгенерированным подклассом унаследованный код выполняется на экземпляре подкласса. Виртуальный вызов другого переопределяемого метода может поэтому диспетчеризоваться на сгенерированное переопределение и активировать аспект. Поведение следует обычной объектно-ориентированной диспетчеризации.
Приватные, final и статические методы остаются вне этого механизма переопределения. Явные вызовы super намеренно обходят переопределения. У конструкторов свои правила.
Результат — не магический универсальный перехват. Это что-то лучшее: перехват, чьё поведение можно объяснить стандартной семантикой Java/Kotlin.
Производительность: итоговая оценка¶
AOP на этапе компиляции улучшает производительность в первую очередь за счёт снижения ненужной работы фреймворка во время выполнения. Он может убрать или снизить обнаружение аннотаций во время выполнения, обобщённые контексты вызова, поиски метаданных, конструирование прокси, рефлексивную диспетчеризацию, временные объекты вызова и обобщённый обход перехватчиков.
Существенное поведение аспекта остаётся. Транзакция всё ещё транзакция. Повтор всё ещё выполняет повторы. Кеш всё ещё выполняет поиски. Валидация всё ещё оценивает правила.
Выигрыш в том, что Kora может присоединять эти поведения к методам через специализированный сгенерированный код, а не через универсальный движок динамического вызова. Это может помочь запуску, установившейся эффективности, давлению выделения, оптимизации JIT, прогреву и предсказуемости выполнения.
Величина зависит от нагрузки, но архитектурное направление ясно.
Отлаживаемость: итоговая оценка¶
Отлаживаемость может быть самым осязаемым преимуществом для повседневной инженерной работы. Сгенерированный прокси можно открыть. Его переопределение можно прочитать. Его конструктор раскрывает зависимости. Его вложенность раскрывает порядок аспектов. Его имена классов и методов могут появляться в производственной диагностике. Его исходный код можно сравнивать между версиями фреймворка.
Это меняет отношения между разработчиком и фреймворком.
Фреймворк больше не говорит:
Доверься, что эта аннотация будет корректно интерпретирована механикой времени выполнения.
Он говорит:
Вот код, произведённый из этой аннотации.
Это более сильная модель прозрачности.
Kora не просто оптимизирует наносекунды вокруг вызова прокси. Она оптимизирует жизненный цикл информации.
Информация, доступная во время компиляции, должна использоваться во время компиляции. Ошибки, обнаруживаемые во время компиляции, должны падать во время компиляции. Структуру, которую можно сгенерировать во время компиляции, не следует заново обнаруживать при запуске. Топологию методов, которую можно выразить как код, не следует оставлять обобщёнными метаданными выполнения. Зависимости, которые можно связать статически, не должны требовать обнаружения во время выполнения.
AOP естественно вписывается в эту философию, потому что размещение аспекта обычно известно из аннотаций исходного кода.
Kora рассматривает эти аннотации как инструкции компилятору, а не запросы контейнеру времени выполнения.
Более глубокий паттерн:
Разработчики хотят высокоуровневые абстракции, потому что писать повторяющуюся инфраструктуру вручную расточительно. Машины выигрывают от специфичного кода, потому что обобщённая интерпретация выполнения имеет затраты. Генерация кода соединяет обе потребности.
Разработчик Kora пишет:
потому что это правильный уровень намерения.
Компилятор создаёт специфичный метод с повтором, потому что это правильный уровень выполнения.
Разработчик не поддерживает повторяющиеся обёртки. JVM не нужен общий AOP-интерпретатор для каждого вызова. Обе стороны получают представление, которое им лучше всего подходит.
Заключение¶
Аспектно-ориентированное программирование часто критикуют за то, что оно делает поток управления невидимым. Эта критика оправдана, когда маленькая аннотация активирует большой механизм времени выполнения, который разработчики не могут легко изучить. Проблема не в самом сквозном поведении. Транзакции, валидация, повторы, кеширование, безопасность, трассировка и логирование действительно принадлежат вокруг бизнес-операций. Проблема в том, чтобы позволить реализации этих задач стать постоянным слоем загадки времени выполнения.
Модель AOP на этапе компиляции Kora идёт другим путём. Аннотации остаются декларативными, но объявление разрешается, пока приложение строится. Kora генерирует конкретный подкласс прокси вокруг перехватываемого компонента, переопределяет соответствующие методы, связывает требуемые зависимости аспектов и помещает окружающее поведение в обычный исходный код. Затем этот сгенерированный класс участвует в графе приложения и выполняется как обычный скомпилированный Java или Kotlin.
Это меняет перехват метода из обобщённой интерпретации выполнения в специализированный поток управления. Это меняет ошибки формы прокси из потенциальных сбоев запуска в диагностику компилятора. Это меняет историю самовызова, потому что унаследованный код выполняется на самом сгенерированном подклассе и поэтому следует обычной виртуальной диспетчеризации. Это снижает объём механики выполнения, нужной только для решения, какие аспекты применяются. Это даёт JIT конкретные методы для оптимизации. И это даёт разработчикам файл исходного кода, который можно открыть, когда поведение неясно.
Поэтому сравнение — не просто между прокси и без прокси. Kora всё ещё использует прокси-подобные сгенерированные классы в архитектурном смысле. Решающее различие в том, когда прокси появляется и какую форму принимает.
Прокси времени выполнения — часть живой механики фреймворка.
AOP-прокси Kora — часть скомпилированной программы.
Этот сдвиг от конструирования во время выполнения к генерации на этапе компиляции меняет модель сбоев, модель производительности, модель отладки и ментальную модель AOP.
Для большинства бэкенд-сервисов структура сквозных задач не по-настоящему динамична. Исходный код уже говорит, какие методы транзакционны, валидируются, кешируются, повторяются, защищены или логируются. Артефакт развёртывания пересобирается всякий раз, когда меняется этот исходный код. В такой среде сохранение универсального движка прокси времени выполнения часто покупает гибкость, которую приложение не использует.
Kora выбирает разрешать то, что может, пока компилятор всё ещё имеет самую богатую информацию и пока сбой всё ещё дёшев.
Разработчик получает аннотацию.
Компилятор получает сложность.
Прод получает код.