Диагностика на этапе компиляции — это функция, а не дополнительная сложность сборки¶
13 сентября 2026
Фреймворки, работающие на этапе компиляции, часто критикуют за то, что они переносят слишком много работы в сборку. Критика поначалу звучит разумно: выполняются процессоры аннотаций, выполняется Kotlin Symbol Processing, сгенерированный исходник появляется в каталогах сборки, IDE должны понимать эти выводы, у Gradle появляется больше задач, а компиляция делает больше, чем просто превращает написанные вручную Java или Kotlin в байткод. С этой точки зрения фреймворк, ориентированный на время выполнения, может казаться проще, потому что видимый конвейер разработки выглядит коротким:
в то время как фреймворк, работающий на этапе компиляции, по-видимому, вставляет ещё один слой:
Эта картина точна, насколько это касается фаз сборки, но вводит в заблуждение как архитектурный вывод. Сложность не исчезает в модели, ориентированной на время выполнения. Графы зависимостей всё равно нужно разрешать, репозиториям всё равно нужны реализации, маршрутам всё равно нужны обработчики, сопоставлениям всё равно нужна исполняемая логика, AOP всё равно нуждается в механизмах перехвата, а валидацию, транзакции, повторы, кеширование и конфигурацию всё равно нужно каким-то образом встроить в приложение. Настоящий архитектурный вопрос не в том, существует ли эта работа, а в том, когда она происходит, где проявляются ошибки, детерминирован ли результат и могут ли разработчики изучить, что фреймворк решил сделать.
Фреймворк Kora — полезный пример, потому что построение графа на этапе компиляции, обработка аннотаций, Kotlin Symbol Processing и генерация исходника — не второстепенные детали реализации. Они центральны для того, как работает фреймворк. Фреймворк пытается проверить зависимости, внедрения, структуру графа, AOP-контракты, сопоставления, объявления репозиториев и сгенерированную инфраструктуру приложения до запуска приложения. В архитектуре, ориентированной на время выполнения, путь к структурной ошибке может выглядеть так:
исходник
↓
сборка успешна
↓
приложение запускается
↓
работа контейнера / прокси / рефлексии
↓
появляется сбой
Kora намеренно пытается перенести большой класс таких сбоев в более ранний цикл:
Смысл не в том, что обработка на этапе компиляции устраняет сложность. Смысл в том, что сложность существует в любом случае. Kora выбирает раскрыть больше её там, где компилятор может её проверить, вместо того чтобы откладывать те же структурные решения до запуска или времени выполнения.
Обработка на этапе компиляции — часть инструментальной цепочки, а не второе время выполнения¶
Фраза процессор аннотаций может заставить механизм звучать более экзотично, чем он есть. Обработка аннотаций Java — часть стандартной инструментальной цепочки компилятора. Процессор получает структурированное представление объявлений исходника, проверяет их и может генерировать дополнительный исходник или ресурсы. Java-разработчики полагаются на этот механизм годами через генераторы внедрения зависимостей, библиотеки сопоставления, сериализаторы, генераторы неизменяемых значений, инструменты конфигурации и генераторы API. Kotlin Symbol Processing выполняет аналогичную роль для Kotlin, предоставляя ориентированную на Kotlin модель объявлений и позволяя инструментам проверять и генерировать код во время сборки.
Архитектурное различие важно: ни Java-процессор, ни KSP не остаются живыми внутри пути запросов продакшена. Это не интерпретаторы, сидящие между бизнес-кодом и JVM. Они выполняются, пока приложение собирается, и их вывод становится обычными скомпилированными классами. С точки зрения приложения времени выполнения процессор исчез. Остаётся код, который он произвёл.
Это значит, что сложность времени сборки и времени выполнения не следует считать эквивалентной. Предположим, фреймворку нужно определить, какая реализация удовлетворяет интерфейсу, содержит ли граф зависимостей цикл, как сопоставляются параметры репозитория, можно ли преобразовать DTO, можно ли переопределить AOP-метод или как HTTP-обработчик должен вызывать контроллер. На эти вопросы где-то нужно ответить. Доступные варианты по сути таковы: ответить на них во время компиляции, во время запуска, при первом использовании или через некоторую смесь этих фаз. Перенос решения раньше не изобретает решение; он меняет его время и качество обратной связи вокруг него.
Это важно, потому что расстояние до сбоя имеет значение. Если зависимость конструктора недопустима и компилятор отклоняет её немедленно, расстояние между причиной и наблюдением короткое:
Если та же ошибка переживает компиляцию и обнаруживается только после упаковки, запуска процесса, инициализации контейнера, разрешения графа и, возможно, конкретного ленивого пути, расстояние гораздо длиннее:
недопустимое объявление зависимости
↓
исходник компилируется
↓
артефакт собран
↓
приложение запускается
↓
контейнер разрешает граф
↓
сбой
Чем длиннее расстояние, тем больше несвязанного состояния системы существует между правкой, вызвавшей проблему, и сигналом, который о ней сообщает. Это увеличивает диагностический шум и часто превращает структурную ошибку в то, что ощущается как операционный сбой. Модель компиляции Kora намеренно сокращает это расстояние везде, где необходимая информация уже доступна.
Более ранний сбой обычно означает более дешёвый сбой¶
Отсутствующая зависимость — самый простой пример. Если фреймворк обнаруживает её во время компиляции, разработчик уже находится в цикле правка-сборка. Сборка падает, диагностика указывает на отсутствующий тип или неразрешённый компонент, и разработчик исправляет объявление. Если та же проблема обнаруживается во время запуска, разработчик уже заплатил за компиляцию, упаковку, запуск JVM, инициализацию фреймворка, построение графа и, возможно, настройку теста или развёртывания, прежде чем получить по сути ту же информацию. Если она появляется только на ленивом пути, цикл исправления становится ещё длиннее.
Те же рассуждения применимы к неоднозначным зависимостям и циклам. У графа приложения есть структурные правила: требуемые зависимости должны существовать, неоднозначность должна быть разрешена, циклы должны отклоняться или обрабатываться явно, типы должны совпадать, а теги или квалификаторы должны быть согласованы. Системы внедрения зависимостей времени выполнения могут проверять эти правила при построении контекста приложения; Kora выбирает проверять их раньше и заставлять сборку падать, когда граф структурно недопустим. Ценность не в том, что Kora изобрела более сильные правила. Ценность в том, что недопустимая архитектура отклоняется ближе к изменению кода, которое её ввело.
Этот принцип естественно выходит за пределы DI. Если маппер генерируется из типов, известных во время компиляции, и модель меняется с String на Instant без допустимого преобразования, мало пользы
откладывать обнаружение до тех пор, пока рефлексивный маппер не выполнится во время выполнения. Если у метода репозитория есть сигнатура, объявление запроса, набор параметров, тип возврата и
требования к сопоставлению, которые все видны во время компиляции, фреймворк может сгенерировать реализацию и позволить компилятору Java или Kotlin проверить большую часть этого контракта до того, как
будет затронута реальная база данных. Это не доказывает, что SQL семантически корректен относительно производственной схемы, и не устраняет необходимость в интеграционных тестах. Это просто создаёт
чистую границу между тем, что можно проверить статически, и тем, что требует реальной инфраструктуры.
То же разделение применимо к AOP. Если метод должен быть переопределяемым, чтобы сгенерированный подкласс мог его обернуть, финальность и видимость метода — структурные факты. Обнаружить это ограничение во время компиляции полезнее, чем обнаружить его при создании прокси времени выполнения. HTTP-маршрутизация следует той же схеме: шаблоны путей, методы, сигнатуры контроллеров, сопоставления запросов и ответов и контракты маршрутов содержат большой объём статической информации. Некоторые ошибки маршрутов всегда будут требовать проверки во время выполнения, но те, которые можно отклонить при генерации обработчиков, следует отклонять там.
Поэтому фреймворк, работающий на этапе компиляции, не пытается превратить каждую возможную проблему в ошибку компилятора. Он пытается не откладывать ошибки, которые уже познаваемы.
Статическая и динамическая валидация должны дополнять друг друга¶
Валидация на этапе компиляции сильнее всего, когда её понимают как один слой в более крупном стеке валидации, а не как замену тестирования или наблюдаемости времени выполнения. Успешная сборка может доказать, что приложение структурно связно согласно информации, доступной во время компиляции. Она не может доказать, что PostgreSQL доступен, что тема Kafka существует, что учётные данные действительны, что удалённый API ведёт себя корректно, что производственные данные удовлетворяют бизнес-предположениям или что конкурентность останется безопасной под нагрузкой.
Здоровая модель выглядит так:
компилятор
→ проверяет статическую архитектуру
компонентные тесты
→ проверяют интеграцию фреймворка
интеграционные тесты
→ проверяют реальные внешние системы
телеметрия времени выполнения
→ проверяет поведение продакшена
Kora усиливает первый слой. Она не убирает последующие.
Это различие важно, потому что иначе «shift left» может стать лозунгом. Полезная версия идеи гораздо точнее: перенесите структурные проверки влево и оставьте подлинно динамические факты там, где им место. Граф зависимостей структурен. Переопределяемость метода структурна. Многие требования к сопоставлению структурны. Доступность базы данных динамична. Состояние брокера Kafka динамично. Бизнес-данные динамичны. Хороший фреймворк не должен путать эти категории.
Когда статические ошибки удаляются раньше, сбои времени выполнения становятся более интересными, потому что с большей вероятностью представляют подлинные проблемы времени выполнения: поведение сети, состояние базы данных, истощение ресурсов, конфигурацию, предоставленную при развёртывании, или бизнес-логику. Запуск также становится более сфокусированным. Вместо смешивания проверки графа, создания прокси, обнаружения метаданных, инициализации приложения и подключения ресурсов в одну большую фазу, большая часть структурной работы уже завершена. Поэтому успешная сборка может дать более сильную уверенность в том, что остающееся при запуске — это действительно работа времени выполнения.
Это сужение пространства проблем имеет операционную ценность. Если сборка уже доказала, что граф корректен и сгенерированный код компилируется, то сбой запуска при подключении к PostgreSQL легче классифицировать. Команда не задаётся одновременно вопросом, не деформирован ли сам граф зависимостей.
CI становится более сильным шлюзом¶
Перенос валидации фреймворка в компиляцию особенно важное влияние оказывает на CI. Конвейер pull request уже компилирует код, запускает тесты и упаковывает артефакты. Если построение графа DI, сопоставления, контракты репозиториев, структура AOP и генерация HTTP-обработчиков участвуют в компиляции, то CI может отклонять структурно недопустимые приложения до того, как будет собран образ контейнера или начнётся развёртывание.
Без этой валидации путь может выглядеть так:
PR сливается
↓
собирается образ
↓
начинается развёртывание
↓
приложение инициализируется
↓
фреймворк обнаруживает структурную проблему
С валидацией на этапе компиляции тот же класс ошибок может остановиться гораздо раньше:
Это меняет экономику сбоев. Контейнеризация может создавать ложное ощущение прогресса: если приложение можно упаковать, но оно не может построить свой граф зависимостей, то пакет никогда не был полезен. Более сильный артефакт сборки — тот, где языковые типы действительны, сгенерированный код фреймворка компилируется, известные статические контракты удовлетворены, а тесты пройдены. Тогда развёртывание может сосредоточиться на специфичных для окружения вопросах вместо повторного обнаружения структурных ошибок, которые уже были видны в исходнике.
Более раннее отклонение также важно в эфемерных CI и агентных рабочих процессах. Сбой компиляции стоит попытки сборки. Сбой развёртывания может стоить создания образа, загрузки в реестр, времени планировщика, запуска, сбора логов, очистки и отката. Чем раньше отклоняется недопустимое изменение, тем дешевле становится неудачный эксперимент.
Это особенно важно, как только нейро-агенты начинают производить множество кандидатных изменений. Машина может итерировать быстро, только если обратная связь приходит быстро и детерминированно. Фреймворк, работающий на этапе компиляции, даёт агенту короткий цикл исправления: сгенерировать код, скомпилировать, прочитать диагностику, исправить изменение и продолжить.
Детерминированные сбои легче воспроизводить¶
Диагностика на этапе компиляции также склонна уменьшать число переменных окружения, участвующих в структурных сбоях. При том же исходнике, classpath, версии процессора и входных данных компилятора результат должен быть тем же. Отсутствующий компонент падает стабильно. Сопоставление, которое нельзя сгенерировать, падает стабильно. Недопустимый сгенерированный исходник воспроизводим из входных данных сборки.
Сбои времени выполнения могут зависеть от гораздо большего числа измерений: профилей, переменных окружения, порядка запуска, внешних ресурсов, ленивой инициализации, путей трафика или метаданных времени выполнения. Не каждый сбой времени выполнения недетерминирован, конечно, и детерминированные системы времени выполнения существуют. Смысл просто в том, что структурная валидация на этапе компиляции сужает поверхность воспроизведения.
Сравните два отчёта об ошибках:
и:
Сервис иногда падает во время запуска,
когда присутствуют эти две автоконфигурации
в производственном профиле.
Первую проблему часто можно воспроизвести, получив исходник и скомпилировав его. Вторая может потребовать реконструкции большей части окружения. Это различие важно для команд приложений, сопровождающих фреймворк, отчётов об ошибках в open source, удалённых команд и автоматизированных агентов.
Ошибки процессоров всё ещё существуют, и их следует рассматривать как ошибки фреймворка. Загадочный стек-трейс процессора, внутреннее исключение или недопустимый сгенерированный код не становятся приемлемыми только потому, что произошли во время компиляции. Но контейнеры времени выполнения, фабрики прокси, мапперы на основе рефлексии и процессоры запуска также могут содержать дефекты. Справедливое сравнение не «процессор может упасть против фреймворка времени выполнения, который никогда не падает». Значимое сравнение — где возникают дефекты, насколько они наблюдаемы и насколько воспроизводим сбой.
Сгенерированный исходник — это источник истины, а не вторая кодовая база¶
Один из самых неправильно понимаемых аспектов Kora — роль сгенерированного исходника. Разработчики иногда видят build/generated и предполагают, что фреймворк создал ещё одну кодовую базу, которую им
теперь нужно понимать и поддерживать. Это не предполагаемая модель.
Сгенерированный код — это производный вывод сборки. Разработчики обычно не редактируют его, не ревьюят как написанный вручную дизайн и не поддерживают напрямую. Отношение владения простое:
Удалите каталог сборки, и сгенерированные файлы можно воссоздать. В этом смысле они напоминают байткод или сгенерированные ресурсы. Разница в том, что у сгенерированного исходника есть полезное гибридное свойство: он одноразовый, как вывод сборки, но читаемый, как код приложения.
Это делает его необычайно эффективной поверхностью для изучения.
Если сгенерированный репозиторий связывает неверный параметр, привязка видима. Если AOP-обёртка вкладывает повтор и таймаут в неожиданном порядке, поток управления видим. Если узел графа выбирает удивительного провайдера, обвязка видима. Если HTTP-обработчик выбирает неожиданный маппер, сгенерированный код может раскрыть, какой именно был выбран.
Это не значит, что разработчики должны рутинно изучать сгенерированный исходник. Большинство разработки приложений должно оставаться на высокоуровневом API. Смысл в том, что когда абстракцию нужно понять точно, фреймворк оставляет после себя нечто конкретное для изучения.
Правильная аналогия — сам компилятор. Java-разработчикам не нужно понимать внутренности javac, чтобы писать на Java, и они не изучают байткод для каждого метода. И всё же байткод, дизассемблеры,
JIT-инструменты и диагностика компилятора доступны, когда нужен более глубокий анализ. Сгенерированный исходник Kora играет аналогичную роль на границе фреймворка. Вам не нужно понимать его, чтобы
использовать Kora, но он доступен, когда нужно изучить точное поведение, которое произвёл фреймворк.
Вот почему сгенерированный исходник следует считать запасным выходом из абстракции, а не обязательным знанием.
Читаемая генерация превращает семантику фреймворка в семантику языка¶
Сгенерированный код становится особенно ценным, когда он сводит специализированное поведение фреймворка к обычному потоку управления Java или Kotlin.
Представьте AOP-обёртку, которая фактически становится:
Порядок аспектов теперь — обычное вложение. Разработчику больше не нужно реконструировать абстрактную цепочку перехватчиков времени выполнения из метаданных контейнера. Или рассмотрим сгенерированную обвязку DI:
Построение графа стало обычным построением объектов.
Сгенерированный исходник может быть многословным, повторяющимся или раскрывать внутренние имена, но его поток управления конкретен. Разработчик может поставить точку останова, проследить вызов метода, изучить поле или сравнить вывод между версиями фреймворка. Эта конкретность часто проще для рассуждений, чем поведение времени выполнения, собранное из прокси, рефлексии, метаданных и состояния контейнера.
Это также делает сгенерированный исходник полезным при обновлениях фреймворка. Примечания к релизам и тесты остаются основными инструментами оценки изменений, но сгенерированный вывод может дать дополнительные факты. Если новая версия Kora меняет порядок обёртывания, размещение телеметрии, привязку запросов или построение компонентов, сгенерированный исходник может раскрыть это различие напрямую. Поведение фреймворка становится диффящимся кодо-ориентированным способом.
То же свойство помогает анализу производительности. Если сгенерированный путь содержит прямые вызовы, а не рефлексию, временные массивы, упаковку или обобщённый вызов, разработчик может изучить этот факт, а не принимать утверждение о производительности полностью на веру. Это делает генерацию исходника механизмом прозрачности, а не просто техникой реализации.
Диагностика компилятора становится частью опыта разработчика¶
Во фреймворке, работающем на этапе компиляции, диагностика компилятора не случайна. Это один из основных способов, которым фреймворк общается с разработчиками, а значит, их качество становится частью пользовательского опыта фреймворка.
Полезная диагностика должна отвечать сразу на несколько вопросов: что упало, где упало, какое объявление вызвало проблему, какой тип или компонент ожидался и почему фреймворк не может продолжить. Сообщение вроде:
ценно, потому что немедленно идентифицирует область проблемы. Сообщение вроде:
делает больше, чем отклоняет код; оно обучает модели перехвата.
Это важно, потому что процессор должен быть невидимым, когда всё работает. Хорошо спроектированный фреймворк, работающий на этапе компиляции, не требует от разработчиков приложений понимания внутренностей процессора. Обычный опыт должен быть таким: написать код приложения, скомпилировать и запустить. Разработчики замечают процессор в основном тогда, когда он ловит недопустимое объявление или когда они намеренно изучают сгенерированный вывод.
Поэтому существует несколько различных уровней знаний:
Уровень 1
использовать публичный API Kora
Уровень 2
изучать сгенерированный исходник при отладке
Уровень 3
изучать реализацию процессора при вкладе в саму Kora
Большинство разработчиков приложений должны оставаться на Уровне 1 большую часть времени. Опытные разработчики могут иногда использовать Уровень 2. Уровень 3 принадлежит в основном вкладчикам фреймворка. Путаница этих уровней заставляет фреймворки, работающие на этапе компиляции, выглядеть гораздо сложнее, чем они есть.
У фреймворков времени выполнения есть аналогичные уровни. Они просто помещают Уровень 2 в другое место: диагностику контейнера, прокси времени выполнения, графы бинов, отладочное логирование, инструменты в стиле actuator или API интроспекции, специфичные для фреймворка. Фреймворки, работающие на этапе компиляции, не изобретают дополнительный слой сложности; они раскрывают один из этих более глубоких слоёв как исходный код.
IDE, Gradle, CI и нейро-технологии могут разделять одну и ту же поверхность обратной связи¶
Ещё одно преимущество диагностики компилятора в том, что её уже понимает окружающая экосистема разработки. IDE понимают файлы, строки, столбцы, символы и сообщения. Gradle понимает упавшие задачи компиляции. CI понимает сбои сборки. нейро-агенты, пишущие код, могут разбирать ту же диагностику и использовать её как сигналы для исправления.
Сгенерированные исходники естественно вписываются в ту же экосистему. Современная разработка на Java и Kotlin уже использует генерацию исходника через Protocol Buffers, генераторы OpenAPI, MapStruct, DI-системы вроде Dagger, инструменты схем и процессоры аннотаций. IDE индексируют сгенерированный код, переходят к сгенерированным реализациям, показывают ошибки компилятора и соответствующим образом помечают сгенерированные каталоги. Gradle моделирует сгенерированный исходник как вывод задачи, потребляемый последующими задачами компиляции, а детерминированные выводы могут участвовать в инкрементальных сборках и кешировании.
Результат — полезная общая поверхность истины:
Нет необходимости в отдельном протоколе здоровья фреймворка только для объяснения структурных ошибок. Фреймворк может использовать конвейер компилятора, которому разработчики уже доверяют.
Это особенно ценно для нейро-технологий. Агент может выдумать неверную аннотацию, неподдерживаемую сигнатуру метода, отсутствующий компонент, недопустимый маппер или невозможную AOP-цель. Во фреймворке, работающем на этапе компиляции, эти галлюцинации становятся задачами исправления, а не скрытыми сбоями времени выполнения. Компилятор отклоняет предложенную структуру, агент получает точный сигнал, и следующая итерация может её исправить.
Сгенерированный исходник даёт следующий уровень фактов, когда одной диагностики недостаточно. Языковые модели сильны в чтении обычного исходного кода и слабее в реконструкции невидимого состояния времени выполнения. Если агент может открыть сгенерированный репозиторий, AOP-обёртку, провайдера графа или HTTP-обработчик, он может рассуждать о конкретной Java или Kotlin, которая реально существует.
Это значит, что прозрачность Kora на этапе компиляции — не только функция отладки для человека. Это также функция машинных рассуждений.
Стоимость сборки реальна, но её нужно оценивать сквозным образом¶
Честное обсуждение диагностики на этапе компиляции должно признать стоимость. Обработка аннотаций и KSP потребляют время сборки. Сгенерированный исходник увеличивает работу компилятора. Большие графы приложений могут сделать обработку более дорогой. Качество инкрементальной обработки важно. Конфигурация Gradle важна. Индексация IDE может стать тяжелее. Объём сгенерированного кода может влиять на использование диска, компиляцию, размер артефакта и загрузку классов.
Архитектура на этапе компиляции не бесплатна.
Релевантный вопрос — где стоимость окупается.
Если процессор добавляет время сборке, но убирает сканирование времени выполнения, построение прокси, метаданные рефлексии, сборку графа при запуске и позднюю валидацию из каждого запуска процесса, компромисс всё равно может быть благоприятным. Одна сборка может произвести артефакт, который запускается многократно в тестах разработчика, тестах CI, стейджинге, производственных развёртываниях, событиях автоскейлинга, перезапусках и замене spot-инстансов. Поэтому работу времени сборки можно амортизировать на множество запусков времени выполнения.
Даже если бы производительность времени выполнения была идентичной, более ранняя диагностика может оправдать некоторую стоимость сборки, потому что сборка даёт более сильный артефакт, а CI отклоняет структурно недопустимые приложения раньше. Выгода не только в скорости; она в определённости.
Правильный показатель производительности также не чистое время процессора в изоляции. И для людей, и для агентов важный цикл обычно таков:
отредактировать один файл
↓
инкрементальная компиляция
↓
запустить целевой тест
↓
проверенное поведение
Фреймворку, работающему на этапе компиляции, нужно сильное инкрементальное поведение, чтобы этот цикл был быстрым. Если каждая крошечная правка заставляет весь граф перегенерироваться и перекомпилироваться, опыт разработчика страдает независимо от того, насколько элегантна архитектура времени выполнения. Поэтому дизайн процессора, кеш-дружелюбные выводы, стабильные входные данные задач и границы мультимодулей важны.
Полное сравнение должно учитывать мысленную модель разработчика, время чистой и инкрементальной сборки, запуск, память времени выполнения, время сбоев, отлаживаемость, воспроизводимость CI, размер сгенерированного артефакта и операционную сложность. Фреймворки, работающие на этапе компиляции, намеренно перемещают стоимость между этими категориями. Ставка Kora в том, что вся система становится проще в эксплуатации и рассуждениях, когда больше структурной работы завершается до времени выполнения.
Сложность времени выполнения не бесплатна только потому, что она менее видима¶
Одна из причин, почему системы на этапе компиляции могут казаться более сложными, в том, что механика времени сборки видима. Разработчики видят зависимости процессоров, плагины KSP, сгенерированные каталоги и задачи Gradle. Механика времени выполнения часто менее видима, потому что происходит внутри запуска фреймворка, инициализации контейнера, рефлексии, фабрик прокси, реестров метаданных или сканирования classpath.
Видимая сложность может психологически ощущаться больше, чем скрытая, но видимость — не то же самое, что количество.
Сканирование времени выполнения потребляет время запуска. Метаданные рефлексии потребляют память. Генерация прокси потребляет работу инициализации. Валидация времени выполнения потребляет часть бюджета сбоев. Состояние контейнера вводит ещё одну поверхность отладки. Тот факт, что эти механизмы не перечислены как явные задачи Gradle, не делает их бесплатными.
Поэтому корректное сравнение — не «простой фреймворк времени выполнения против сложного фреймворка на этапе компиляции». Это сравнение всей системы того, где выполняется одна и та же архитектурная работа и какую форму она принимает после выполнения.
Kora намеренно делает больше этой работы видимой и детерминированной во время компиляции.
Запуск становится больше о запуске, чем об обнаружении архитектуры¶
Модель диагностики на этапе компиляции тесно связана с поведением запуска.
Контейнер, ориентированный на время выполнения, может тратить время запуска на сканирование, обнаружение компонентов, разрешение зависимостей, проверку структуры графа, создание прокси и построение метаданных. Приложение на этапе компиляции может перенести часть этой работы раньше, чтобы запуск времени выполнения был ближе к загрузке предсобранного графа, построению компонентов, инициализации ресурсов и переходу в состояние готовности.
Вот почему модель диагностики и модель производительности запуска усиливают друг друга. Перенос разрешения графа и генерации кода в сборку не просто даёт более ранние ошибки; это также означает, что при каждом запуске сервиса остаётся меньше структурной интерпретации.
Для статически развёртываемых бэкенд-сервисов это разделение естественно. Большинство сервисов не добавляют динамически произвольные реализации репозиториев во время работы. Их архитектура меняется через знакомый цикл:
Если структура контроллеров, реализации репозиториев, требования к мапперам и применимость AOP уже известны при сборке артефакта, мало пользы в повторном обнаружении этих фактов при каждом запуске.
Время выполнения должно тратить большую часть времени на вещи, которые действительно являются вопросами времени выполнения: загрузку конфигурации развёртывания, открытие сетевых ресурсов, подключение к базам данных и брокерам, запуск серверов и обработку трафика.
Архитектуру на этапе компиляции лучше всего понимать как компиляцию архитектуры¶
Полезная мысленная модель — что Kora компилирует больше, чем синтаксис языка. Она компилирует часть архитектуры приложения.
Конвейер сборки берёт типы, аннотации, модули, контракты репозиториев, HTTP-объявления и другую статическую информацию и производит обвязку графа, репозитории, сопоставления, обработчики и AOP-обёртки. Рассмотренная так, обработка аннотаций — не вспомогательная магия сборки. Это компилятор архитектуры фреймворка.
Эта идея не необычна в современной разработке. Protocol Buffers берут схему и производят языковые типы. Генераторы OpenAPI берут контракт API и производят клиенты или интерфейсы серверов. Инструменты схем выводят строго типизированные модели из определений данных. Инструменты native-image выполняют анализ заранее. Общая идея — использовать информацию, которая уже известна раньше, чтобы производить более сильные артефакты.
Kora применяет этот принцип к инфраструктуре фреймворка.
Это также объясняет, почему обработку аннотаций не следует описывать как дополнительный слой абстракции времени выполнения. Она не сидит в пути запросов продакшена. Она не перехватывает каждый вызов контроллера. Она не интерпретирует метаданные фреймворка, пока сервис обрабатывает трафик. Она выполнялась раньше и произвела код.
Более точный конвейер:
декларативный исходник приложения
↓
процессор Kora / KSP
↓
обычный сгенерированный Java/Kotlin
↓
обычная компиляция
↓
время выполнения с меньшей интерпретацией фреймворка
Сборка становится более сложной, чтобы выполнение времени выполнения могло стать более конкретным.
Лучший сбой — тот, который предотвращает недопустимый артефакт¶
Есть качественная разница между производством артефакта приложения, который не может запуститься, и отказом производить артефакт вообще.
Если граф зависимостей структурно недопустим, маппер не может быть сгенерирован или AOP-цель не может удовлетворить правила фреймворка, то «код не собирается» часто является самым чистым возможным исходом. CI останавливается немедленно. Ничего не упаковывается. Ничего не развёртывается. Ничто не выглядит здоровым перед сбоем. Недопустимое состояние никогда не становится кандидатом на релиз.
Это один из самых сильных аргументов за диагностику на этапе компиляции, потому что он превращает ошибки фреймворка в обычные ошибки сборки.
Отсутствующий DI-компонент становится сбоем сборки. Недопустимое сопоставление становится сбоем сборки. Невозможная AOP-цель становится сбоем сборки. Известная структурная недопустимость принадлежит ровно туда.
Это также улучшает коммуникацию в команде. Pull request, который падает, потому что PaymentClient неоднозначен, легче обсуждать, чем развёртывание, которое начинает падать в конкретном окружении с
длинным стек-трейсом инициализации. Перенос структурных ошибок в CI снижает стоимость координации между разработчиками, ревьюерами и эксплуатацией.
Чем ближе обнаружение к правке, которая ввела ошибку, тем дешевле становится цикл исправления.
Валидация на этапе компиляции улучшает введение в проект¶
Ранняя диагностика также делает фреймворк более лёгким для изучения.
Новичку не нужно запоминать каждое правило графа перед первым изменением. Он может учиться через сборку. Если он что-то неправильно связал, компилятор это отклоняет. Если метод нельзя использовать как AOP-цель, ошибка раскрывает часть модели фреймворка. Если требуемый маппер недоступен, сбой указывает прямо на отсутствующий структурный элемент.
Фреймворк становится управляемой системой, а не сводом правил, которые нужно запомнить заранее.
Это важно и для введения людей, и для разработки при поддержке нейро-технологий по одной и той же причине: оба работают лучше всего, когда недопустимый путь короток.
— гораздо более сильный цикл обратной связи, чем:
недопустимый исходник
↓
успешная сборка
↓
упаковка
↓
развёртывание
↓
запуск
↓
сбой времени выполнения
Альтернатива сложности на этапе компиляции часто не меньшая сложность. Это отложенная сложность.
Сгенерированный исходник и диагностика образуют многослойную модель отладки¶
Архитектуру Kora на этапе компиляции проще всего понять как многослойную модель отладки, а не как требование, чтобы разработчики читали внутренности процессора.
Первый сигнал — диагностика компилятора. Если проблема в отсутствующем компоненте, недопустимом сопоставлении или невозможной AOP-цели, сборка может уже объяснить её. Следующий слой — само объявление приложения. Если требуется более глубокое изучение, сгенерированный исходник показывает, что произвёл фреймворк. Исходник фреймворка нужен только тогда, когда сгенерированный вывод выглядит неверным или подозревается дефект фреймворка. Компонентные и интеграционные тесты проверяют поведение относительно реальных зависимостей, а телеметрия времени выполнения занимается вопросами, относящимися только к продакшену.
Практическая иерархия выглядит так:
1. Прочитайте диагностику компилятора.
2. Прочитайте объявление приложения.
3. Изучите сгенерированный исходник при необходимости.
4. Изучите исходник фреймворка только для более глубокого анализа фреймворка.
5. Запустите компонентные/интеграционные тесты.
6. Изучите телеметрию времени выполнения для динамических сбоев.
Большинство разработчиков приложений должны редко достигать самых глубоких уровней. Это здоровое наслоение сложности.
Важный принцип в том, что сгенерированный исходник и диагностика отвечают на разные вопросы. Диагностика отвечает: «Почему сборка упала?» Сгенерированный исходник отвечает: «Что фреймворк делает, когда сборка успешна?» Вместе они делают фреймворк необычайно доступным для изучения, не заставляя каждого разработчика становиться вкладчиком фреймворка.
Нейро-агенты выигрывают от этой модели непропорционально¶
Диагностика на этапе компиляции особенно хорошо согласуется с нейро-агентами, пишущими код, потому что естественный цикл агента уже выглядит как управляемый компилятором цикл исправления:
Диагностика компилятора лаконична, локальна, детерминирована и машиночитаема. Если агент выдумывает неверную аннотацию, неподдерживаемую сигнатуру метода, отсутствующий компонент, недопустимый маппер или невозможную AOP-цель, фреймворк отклоняет предложение и превращает галлюцинацию в конкретную задачу исправления.
Сгенерированный исходник усиливает цикл дальше, потому что даёт модели точное поведение фреймворка, когда одной диагностики недостаточно. Агент может изучить сгенерированный репозиторий, провайдера графа, AOP-обёртку или HTTP-обработчик и рассуждать об обычной Java или Kotlin, а не пытаться выводить невидимое состояние времени выполнения из общих знаний о фреймворке.
Это важное различие. Модели на основе нейро-технологий очень хороши в чтении кода и гораздо менее надёжны, когда им приходится реконструировать динамическое состояние контейнера, которое они не могут напрямую наблюдать. Поэтому архитектура Kora на этапе компиляции оказывается высокосовместимой с агентной разработкой без необходимости в специальной среде выполнения с нейро-технологиями. Обычной инструментальной цепочки — Gradle, компиляции Java/Kotlin, обработки аннотаций или KSP, сгенерированного исходника и тестов — достаточно.
Люди и агенты потребляют одни и те же сигналы. Разработчик видит ошибку компилятора; агент видит ту же ошибку. Разработчик может открыть сгенерированный исходник; агент может изучить тот же исходник. Разработчик запускает компонентный тест; агент может запустить тот же тест. Нет отдельной объяснительной системы для машин.
Это мощное архитектурное свойство.
Гибкость времени выполнения всё ещё имеет законное применение¶
Ничто из этого не означает, что композиция на этапе компиляции универсально превосходна.
Динамические системы плагинов могут нуждаться в обнаружении реализаций после времени сборки. Приложения могут намеренно загружать неизвестные классы или скрипты во время выполнения. Высококонфигурируемые платформы могут нуждаться в том, чтобы решения о композиции оставались открытыми до развёртывания или даже до выполнения. В таких средах обнаружение во время выполнения и рефлексия могут быть правильным инженерным выбором.
Kora оптимизирована под другой контекст: статически собираемые бэкенд-сервисы, чья архитектура меняется через исходник, сборку и развёртывание. Для этой модели разрешение графа на этапе компиляции — естественное соответствие, потому что структура компонентов сервиса уже известна при сборке артефакта.
Поэтому правильный вопрос не в том, всегда ли обработка на этапе компиляции лучше. Он в том, соответствует ли архитектура модели развёртывания. Для обычных облачных бэкенд-сервисов ответ часто «да».
Настоящий вопрос — сложность всей системы¶
Архитектуру фреймворка не следует оценивать, считая задачи Gradle, сгенерированные файлы или прокси времени выполнения в изоляции. Значимое сравнение — вся система.
Фреймворк, ориентированный на время выполнения, может иметь более лёгкую структурную сборку, но выполнять больше сканирования, рефлексии, построения прокси, валидации и сборки графа во время запуска или вызова. Фреймворк на этапе компиляции может тратить больше CPU во время сборки в обмен на более раннюю диагностику, более сильные артефакты, более быстрый запуск, более простые пути времени выполнения, детерминированный сгенерированный код и лучшую доступность для изучения.
Компромисс можно резюмировать так:
Подход на этапе компиляции
больше структурной работы во время сборки
↓
более сильный артефакт
↓
меньше структурного обнаружения при запуске
↓
более ранние сбои
против:
Подход, ориентированный на время выполнения
более лёгкая структурная сборка
↓
больше работы фреймворка во время запуска/выполнения
↓
более поздняя структурная валидация
Ни одна архитектура не универсально корректна. Kora намеренно предпочитает первую для статически развёртываемых бэкенд-сервисов.
Самая сильная версия аргумента поэтому не в том, что диагностика на этапе компиляции бесплатна, и не в том, что сгенерированный код устраняет всю отладку. Она в том, что одна и та же архитектурная сложность должна где-то существовать, и Kora выбирает материализовать больше её в фазе, где её можно проверить, воспроизвести, закешировать, изучить и отклонить до развёртывания.
Заключение¶
Диагностику на этапе компиляции часто преподносят как цену, которую разработчики платят за фреймворк, делающий слишком много во время сборки. Такая постановка упускает архитектурную отдачу.
Внедрение зависимостей всё равно должно быть разрешено где-то. Сопоставлениям всё ещё нужны исполняемые реализации. Репозиториям всё ещё нужен реальный код. AOP всё ещё нужны обёртки. HTTP-маршрутам всё ещё нужны обработчики. Валидации всё ещё нужна логика. Фреймворк, ориентированный на время выполнения, может собрать большую часть этого, пока приложение запускается или работает. Kora выбирает собирать и проверять больше этого, пока приложение собирается.
Это меняет время сбоев, а время сбоев имеет значение.
Для статически развёртываемого бэкенд-сервиса структурная ошибка обычно дешевле всего, когда она обнаруживается рядом с правкой исходника, которая её вызвала. Компилятор может отклонить приложение до упаковки. CI может остановиться до создания развёртываемого артефакта. Те же исходник и classpath могут воспроизводить сбой стабильно. Сгенерированный вывод можно изучать как источник истины. Запуск может сильнее сосредоточиться на реальной работе времени выполнения, потому что большая часть архитектуры уже решена.
Ничто из этого не означает, что разработчикам нужно понимать внутренности процессора. Обычно им это не нужно. Точно так же, как Java-разработчики не отлаживают javac каждый раз, когда компилятор
отклоняет вызов метода, пользователи Kora обычно должны видеть действенную диагностику и исправлять код приложения. Сгенерированный исходник — это необязательная глубина, а не обязательное знание. Он
существует, чтобы, когда абстракцию фреймворка нужно понять точно, ответ не был полностью скрыт в состоянии времени выполнения.
Компромисс реален. Сборки делают больше работы. Производительность процессора важна. Инкрементальная компиляция важна. Объём сгенерированного кода важен. Кеши сборки важны. Но эти затраты покупают более сильные артефакты, более ранние ошибки, детерминированную диагностику, меньше обнаружения времени выполнения, более быстрый запуск и доступное для изучения поведение.
Для людей это даёт более короткий цикл обратной связи. Для CI это создаёт более сильный шлюз. Для нейро-агентов это создаёт необычайно эффективный цикл исправления, потому что компилятор может отклонять плохие предположения, а сгенерированный исходник может раскрывать точную интерпретацию фреймворка.
Архитектурный момент в конечном счёте прост:
Сложность существует в любом случае. Kora выбирает раскрыть её там, где компилятор может её проверить, вместо того чтобы прятать её до времени выполнения.
И самый чистый способ думать об обработке аннотаций в Kora таков:
Обработка аннотаций — не дополнительный слой абстракции времени выполнения. Это механизм, который удаляет слои абстракции времени выполнения, превращая поведение фреймворка в обычный скомпилированный код.