Внедрение зависимостей на этапе компиляции: как Kora строит приложение¶
3 августа 2026
Внедрение зависимостей часто представляют как механизм удобства: вместо ручного конструирования каждого объекта класс объявляет, что ему нужно, а контейнер это предоставляет. Это описание полезно, но оно скрывает более интересный архитектурный вопрос: когда контейнер решает, чем на самом деле является приложение?
Во многих JVM-фреймворках значительная часть этой работы происходит во время выполнения. Контейнер запускается, обнаруживает компоненты, анализирует конструкторы и фабричные методы, разрешает зависимости, применяет квалификаторы, проверяет циклы, создаёт внутренние определения и только затем собирает граф объектов. Kora Framework намеренно переносит большую часть этого рассуждения в компиляцию.
Его движок внедрения зависимостей лучше всего понимать как компилятор графа на этапе компиляции, поддерживаемый небольшим исполнителем графа во время выполнения. Во время компиляции Kora обнаруживает провайдеров, разрешает запросы зависимостей, проверяет неоднозначность, верифицирует теги и циклы, генерирует недостающие компоненты фреймворка и производит граф приложения. Во время выполнения контейнер исполняет этот уже известный граф: он создаёт объекты, инициализирует компоненты с поддержкой жизненного цикла, управляет ссылками, обновляет затронутые узлы при необходимости и корректно завершает всё в порядке зависимостей.
Это разделение — ключ к пониманию DI в Kora.
DI как компиляция графа¶
Рассмотрим небольшое приложение:
С точки зрения движка DI эти конструкторы описывают граф:
Каждый конструируемый компонент становится узлом, а каждый параметр конструктора или фабрики становится требованием зависимости, которое должно быть разрешено в другой узел.
Например:
можно читать как запрос ровно одного компонента, совместимого с OrderRepository.
Тегированная зависимость:
запрашивает ровно один соответствующий OrderRepository с требуемым тегом.
Зависимость в виде коллекции:
меняет запрос снова: вместо ровно одного провайдера конструктор запрашивает все соответствующие компоненты Handler.
Контейнер времени компиляции многократно разрешает эти требования, пока не построит достижимый граф приложения. Если требуемый провайдер не существует, если несколько одинаково валидных провайдеров конкурируют за одну точку внедрения или если жёсткий цикл зависимостей делает конструирование невозможным, Kora останавливает компиляцию, а не откладывает ошибку до запуска.
Это делает внедрение зависимостей менее похожим на поиск во время выполнения и больше похожим на статическое разрешение графа.
@KoraApp: корень графа приложения¶
Каждое приложение Kora начинается с интерфейса, аннотированного @KoraApp:
Этот интерфейс определяет корневую границу сборки приложения. Он может содержать фабричные методы напрямую или наследовать модули, которые вносят инфраструктуру и провайдеров уровня приложения.
Более реалистичное приложение могло бы выглядеть так:
Это не просто организационный синтаксис. Это сообщает компилятору, какие внешние модули являются частью вселенной зависимостей приложения.
Kora намеренно избегает модели, в которой каждая зависимость classpath сканируется, а произвольные определения фреймворка присоединяются неявно. Компоненты и модули из текущей области компиляции могут
участвовать напрямую, тогда как внешние модули подключаются через явное наследование от интерфейса приложения. Для многомодульных сборок @KoraSubmodule предоставляет дополнительную границу
времени компиляции.
Во время обработки аннотаций Kora использует интерфейс @KoraApp как отправную точку для генерации ApplicationGraph. Запуск затем напрямую ссылается на этот сгенерированный граф:
Поэтому граф — не структура, обнаруженная во время выполнения. Это скомпилированное представление того, как должно собираться приложение.
@Component: объявление конструируемого узла¶
Самый простой способ сделать класс приложения доступным для DI — @Component:
Это объявление даёт Kora два фрагмента информации. Во-первых, PaymentService может стать узлом графа. Во-вторых, его конструктор сообщает компилятору, какие другие узлы требуются для его создания.
Kora держит эту модель намеренно простой. Java-компонент имеет публичный конструктор, а его параметры конструктора описывают его зависимости. Фреймворк в целом не пытается инстанцировать произвольные классы просто потому, что они оказываются конструируемыми.
Это различие важно. Предположим, мы пишем:
но ни компонент, ни фабрика модуля, ни обобщённый провайдер, ни расширение времени компиляции никогда не поставляют PaymentRepository. Kora не решает молча инстанцировать какой-то подходящий класс.
Граф зависимостей
неполон, поэтому компиляция завершается ошибкой.
Эта модель явной регистрации делает каждый узел прослеживаемым до конкретного механизма провайдера, а не позволяет контейнеру изобретать структуру приложения из случайного содержимого classpath.
Фабричные методы: провайдеры без @Component¶
Не каждый объект может или должен быть аннотирован.
Сторонние библиотеки — очевидный случай. Вам может понадобиться ExecutorService, клиент из внешнего SDK или какой-то ресурс, создаваемый через builder API. Kora обрабатывает это через фабричные
методы:
Для компилятора графа эти фабричные методы ведут себя точно так же, как провайдеры на основе конструкторов. Первый вносит узел ExecutorService без зависимостей. Второй вносит узел
JobService, который зависит от этого исполнителя.
Параметры фабрики разрешаются по тем же правилам, что и параметры конструктора, поэтому движку DI не нужна отдельная концептуальная модель для классов приложения и внешне созданных объектов.
Это одна из причин, по которой контейнер Kora остаётся относительно лёгким для рассуждения: логика конструирования выражается как обычные методы Java или Kotlin, а компилятор графа использует эти методы как определения провайдеров.
@Module: группировка связанных провайдеров¶
По мере роста приложений размещение каждого фабричного метода напрямую на @KoraApp становится громоздким. @Module позволяет группировать связанные провайдеры:
Эти методы остаются обычными провайдерами компонентов. Модуль просто даёт им связную границу.
Приложение затем может явно компоновать модули:
Это делает композицию модулей саму по себе информацией времени компиляции. Библиотека может публиковать модули провайдеров, а приложение выбирает, какие из них станут частью его графа, не полагаясь на хуки регистрации во время выполнения или глобальное обнаружение.
Это важно для прозрачности. Взгляд на интерфейс @KoraApp рассказывает вам значимую часть архитектурной композиции приложения до того, как сервис когда-либо запустится.
Достижимость: не каждый провайдер становится компонентом рантайма¶
Граф Kora — не обязательно реестр каждого объявления компонента, которое он может обнаружить. Что в конечном счёте важно — какие узлы достижимы из корней приложения.
Рассмотрим:
Если ни один корень или достижимый компонент не зависит от DebugReporter, нет сильной причины инстанцировать его. Простое существование в исходном коде не автоматически подразумевает, что он должен
участвовать в
инициализации рантайма.
Инфраструктура, которой нужно запускаться независимо, вместо этого может быть помечена как корень:
HTTP-серверы, фоновые рабочие, потребители, планировщики и подобные компоненты часто ведут себя как корни графа, потому что представляют активные точки входа в работающую систему.
Это даёт DI в Kora модель, ориентированную на достижимость, а не простой глобальный реестр бинов. Результирующий граф содержит то, что приложению фактически нужно для работы.
Разрешение зависимостей как точное сопоставление¶
Как только Kora обнаружил возможных провайдеров, начинается настоящая работа разрешения.
Предположим, у нас есть:
Компилятор фактически видит требование зависимости, описанное типом, тегами и кардинальностью. В этом случае требование простое: разрешить ровно один PaymentGateway.
Если есть один валидный провайдер, Kora создаёт ребро. Если их нет, компиляция завершается ошибкой. Если есть несколько одинаково валидных кандидатов, зависимость неоднозначна, и компиляция также завершается ошибкой.
Например:
в сочетании с:
не содержит достаточно информации, чтобы идентифицировать задуманного провайдера.
Kora не угадывает на основе порядка объявления, порядка classpath, соглашений об именах или какого-то неявного ранжирования. Приложение выразило неоднозначную архитектуру, поэтому компилятор сообщает об этом.
Это важный дизайнерский выбор. Внедрение зависимостей остаётся детерминированным, потому что неоднозначность должна моделироваться явно, а не скрываться за эвристиками контейнера.
Теги — часть идентичности зависимости¶
Теги решают случаи, когда несколько компонентов разделяют один и тот же тип Java, но служат разным ролям.
Предположим, есть два шлюза:
Потребитель может указать, какой из них ему требуется:
Архитектурно Kora больше не разрешает только тип Java. Он разрешает тип вместе с набором тегов.
Это делает теги больше, чем косметическими квалификаторами. Они часть идентичности запроса зависимости.
Использование классов как тегов вместо строковых имён также держит эти различия внутри обычной системы типов Java. Теги можно просматривать, переименовывать и рефакторить через обычные инструменты, вместо того чтобы становиться хрупкими строковыми идентификаторами.
Одна деталь особенно важна: нетэгированная точка внедрения не означает «любой компонент этого типа». Она конкретно запрашивает нетэгированный вариант. Если приложение намеренно хочет
игнорировать различия тегов, Kora предоставляет Tag.Any для этой цели.
Это держит семантические границы явными. Клиент, настроенный для одной базы данных, арендатора, региона или транспорта, не должен случайно удовлетворять несвязанную точку внедрения просто потому, что сырой тип Java совпадает.
All<T>: когда корректно более одного совпадения¶
Неоднозначность — ошибка только тогда, когда зависимость ожидает один компонент.
Иногда приложение действительно хочет каждую реализацию.
Представьте:
с несколькими реализациями:
Диспетчер может запросить их все:
Это меняет зависимость с единственного числа на множественное.
Обычный NotificationChannel требует ровно одного совместимого провайдера. All<NotificationChannel> разрешает каждый соответствующий провайдер и раскрывает их через итерируемую коллекцию.
В отличие от обычной сингулярной зависимости, All<T> также может валидно быть пустым. Это делает его полезным для необязательных точек расширения, паттернов в стиле плагинов, обработчиков событий,
валидаторов, перехватчиков или любого
другого сценария, где приложение хочет собрать ноль или более реализаций.
Теги естественно сочетаются с All<T>:
в то время как @Tag(Tag.Any.class) позволяет коллекции намеренно охватывать тегированные и нетэгированные реализации.
Поэтому модель DI имеет небольшой, но выразительный словарь кардинальности: обычные зависимости означают ровно один, обнуляемые или необязательные зависимости означают ноль или один, а All<T>
означает от нуля до многих.
Значения по умолчанию и переопределения приложения¶
Библиотекам часто нужно предоставлять реализацию по умолчанию, не мешая приложениям заменять её.
Kora моделирует это с помощью @DefaultComponent:
Приложение затем может предоставить другой компонент с тем же типом и тегами. Построитель графа рассматривает значение по умолчанию как запасной вариант, а не как равного кандидата, поэтому явная реализация уровня приложения побеждает.
Это избегает превращения кастомизации в механизм замены бинов во время выполнения. Модуль может поставлять разумные значения по умолчанию, при этом переопределение остаётся обычной композицией на уровне исходного кода.
Отсутствующие зависимости становятся ошибками компиляции¶
Одно из самых ясных следствий DI времени компиляции — местоположение отказа.
Предположим:
но приложение не предоставляет PetRepository.
В модели DI времени выполнения эта ошибка может пережить компиляцию, упаковку, создание образа, развёртывание и запуск процесса, прежде чем контейнер наконец сообщит, что граф не может быть построен.
Kora уже знает во время обработки аннотаций, что валидного провайдера не существует. Поэтому компиляция останавливается.
Более важно, у компилятора достаточно информации о графе, чтобы объяснить, почему требовалась зависимость. Он может сообщить отсутствующий тип, точку внедрения и путь зависимости, ведущий от корня графа к неразрешённому узлу.
Концептуально отказ можно понять так:
Этот путь графа полезен, потому что место, где разрешение терпит неудачу, может быть на несколько зависимостей удалено от корня, который сделал узел достижимым в первую очередь.
Поэтому DI времени компиляции улучшает не только то, когда происходит ошибка, но и контекст, доступный для её объяснения.
Циклы обнаруживаются как структура графа¶
Циклы — естественная проблема графа.
Валидная цепочка зависимостей может выглядеть так:
в то время как случайный цикл выглядит так:
Например:
Поскольку Kora разрешает граф во время компиляции, он может обнаружить эту структуру до запуска приложения.
Для жёстких зависимостей, которые нельзя проксировать или отложить, цикл — ошибка времени компиляции. Kora может сообщить фактический путь цикла, а не позволять сервису падать только после того, как создание объектов уже началось.
Фреймворк может разрешать некоторые циклы на основе интерфейсов через сгенерированные ленивые прокси, но это не меняет лежащую в основе архитектурную точку: циклы видимы компилятору графа.
Во многих случаях более чистое решение — не полагаться на проксирование вообще, а изменить природу одного из рёбер. Здесь важным становится ValueOf<T>.
ValueOf<T> меняет смысл ребра зависимости¶
ValueOf<T> может изначально выглядеть как абстракция провайдера:
которую позже можно разыменовать:
Но в модели графа Kora оно представляет нечто более важное: косвенную зависимость, которая не участвует в распространении жизненного цикла так же, как прямая зависимость.
Рассмотрим:
Прямая зависимость означает, что ServiceC построен против конкретного экземпляра ServiceB. Если граф позже обновляет ServiceB, нижестоящие компоненты, которые напрямую зависят от него, также
могут нуждаться в
пересборке.
Теперь измените конструктор на:
ServiceC теперь держит ссылку на узел графа, а не на фиксированный экземпляр ServiceB. Вызов serviceB.get() разрешает текущее значение. Если ServiceB обновляется, сам ServiceC может
оставаться живым.
Это делает ValueOf<T> полезным на архитектурных границах, где один компонент должен наблюдать изменения в другом, не разделяя его жизненный цикл.
Долгоживущий HTTP-сервер — хороший пример. Сервер владеет слушающим сокетом и не должен перезапускаться просто потому, что изменилась некоторая структура нижестоящего обработчика. Удержание ValueOf
на
эту изменяющуюся зависимость позволяет серверу оставаться стабильным, при этом наблюдая последнее значение графа.
Тот же механизм может разорвать жёсткий цикл:
Отношение всё ещё существует семантически, но это больше не жёсткое ребро конструирования в том же смысле.
Это показывает, что граф Kora моделирует больше, чем ссылки на объекты. Он также моделирует распространение жизненного цикла.
Граф можно обновлять во время выполнения¶
DI времени компиляции не означает, что каждое значение графа навсегда заморожено после компиляции.
Форма графа известна заранее, но некоторые узлы могут обновляться во время выполнения.
Обновляемый граф Kora может пересобрать узел и напрямую затронутую нижестоящую часть графа. Заменяющий подграф создаётся и инициализируется сначала, и только затем подменяется в работающее приложение. Если инициализация терпит неудачу, временный граф освобождается, а предыдущий граф остаётся активным.
Эта транзакционная модель обновления делает ValueOf<T> особенно важным, потому что она определяет, где останавливается распространение обновления.
Предположим, у нас есть:
Если каждое ребро прямое, обновление конфигурации может требовать пересборки нескольких нижестоящих узлов.
Если долгоживущий компонент вместо этого зависит от:
этот компонент может пережить, наблюдая обновлённый экземпляр репозитория.
Поэтому граф статичен по структуре, но не обязательно статичен по значению. Kora валидирует топологию на этапе компиляции, а затем позволяет операциям рантайма действовать в пределах этой валидированной топологии.
Жизненный цикл следует за графом зависимостей¶
Тот же граф, используемый для внедрения, также говорит Kora, как приложение должно запускаться и останавливаться.
Рассмотрим:
База данных должна быть готова до репозитория, репозиторий до сервиса, а сервис до того, как сервер начнёт принимать трафик. Во время завершения порядок должен быть обратным.
Kora может естественно вывести это из зависимостей графа.
Компоненты, владеющие ресурсами, могут реализовать:
Например:
Контейнер инициализирует зависимости до их зависимых и освобождает их в обратном порядке. AutoCloseable также интегрируется с завершением, а объекты, созданные фабрикой, могут быть обёрнуты, когда
исходный тип не может реализовать интерфейс жизненного цикла Kora напрямую.
Это делает жизненный цикл свойством графа приложения, а не полностью отдельной подсистемой запуска.
Параллельная инициализация выпадает естественно¶
Как только граф известен, Kora также знает, какие ветви независимы.
Предположим, граф запуска выглядит так:
Ветвь базы данных и ветвь Kafka не зависят друг от друга, поэтому они могут инициализироваться конкурентно, пока не достигнут узла, требующего обеих.
Поскольку порядок зависимостей явный, запуск рантайма может планировать готовые узлы параллельно, вместо серийной инициализации всего приложения.
Это недооценённая выгода построения графа на этапе компиляции. Граф — не только структура поиска DI; это также план исполнения для инициализации и завершения.
Поэтому Kora может инициализировать независимые узлы жизненного цикла на виртуальных потоках, сохраняя при этом корректность зависимостей.
@KoraSubmodule: масштабирование DI по модулям сборки¶
Большие приложения часто охватывают несколько Gradle-модулей:
@KoraSubmodule Kora предоставляет границу времени компиляции для этих случаев.
Модуль может объявить:
Kora обрабатывает компоненты и модули внутри этой единицы компиляции и генерирует интерфейс подмодуля, необходимый приложению.
Главное приложение затем компонует их:
Во время выполнения по-прежнему существует один граф приложения, но работа обнаружения организована вокруг границ компиляции.
Это важно для инкрементальных сборок, потому что изменения внутри одного модуля проекта не обязательно требуют повторного обнаружения каждого компонента во всей кодовой базе. Поэтому граница модуля помогает не только организации кода, но и масштабируемости компиляции DI.
Функции фреймворка используют тот же движок DI¶
Контейнер не ограничен написанными вручную классами приложения.
Многие интеграции Kora участвуют в том же процессе разрешения.
Предположим, сгенерированному HTTP-обработчику требуется:
но приложение не объявило такой компонент явно.
Расширение времени компиляции Kora может распознать, что запрошенная зависимость генерируема, создать необходимый JSON-писатель и вернуть результирующего провайдера обратно в граф DI.
Концептуально:
нужен JsonWriter<OrderResponse>
↓
нет явного провайдера
↓
спросить расширения времени компиляции
↓
JSON-процессор может его сгенерировать
↓
сгенерировать $OrderResponse_JsonWriter
↓
зарегистрировать сгенерированного провайдера
↓
продолжить разрешение графа
Тот же паттерн используется репозиториями, декларативными HTTP-клиентами, маппингами конфигурации, валидацией, gRPC и другими сгенерированными функциями.
Это одна из самых важных архитектурных деталей в Kora. Компилятор DI и подсистемы генерации кода сотрудничают. Сгенерированная инфраструктура фреймворка становится обычными узлами графа, а эти узлы разрешаются по тем же правилам типов и тегов, что и написанные вручную компоненты.
DI времени компиляции как валидация архитектуры¶
Когда все эти механизмы объединены, компиляция становится больше, чем проверкой синтаксиса и типов.
Kora может верифицировать такие вопросы, как: есть ли у каждой зависимости провайдер, неоднозначна ли зависимость, совпадают ли теги, конструируемы ли корни графа, существуют ли жёсткие циклы, может ли быть произведена требуемая сгенерированная инфраструктура и может ли приложение сформировать валидный граф жизненного цикла.
Это архитектурные свойства.
Традиционный DI-фреймворк времени выполнения может не отвечать на них, пока не создан контекст приложения. Kora отвечает на большую их часть, пока компилятор уже обрабатывает исходный код.
Поэтому граница отказа смещается раньше:
модель, ориентированная на рантайм:
компиляция
→ упаковка
→ сборка образа
→ развёртывание
→ запуск JVM
→ построение DI-контейнера
→ отказ
становится:
Вот почему DI времени компиляции не следует сводить к оптимизации запуска.
Более быстрый запуск ценен, но более глубокая выгода в том, что недопустимый граф приложения рассматривается гораздо больше как недопустимый исходный код.
Что контейнер рантайма всё ещё делает¶
У Kora всё ещё есть контейнер зависимостей времени выполнения.
Его работа просто другая.
На этапе компиляции Kora определяет, какие провайдеры существуют, как разрешаются зависимости, как совпадают теги и кардинальности, какие компоненты достижимы, где существуют циклы и как связан финальный граф.
Во время выполнения исполнитель графа создаёт синглтоны, инициализирует компоненты с поддержкой жизненного цикла, хранит состояние узлов, обслуживает косвенные ссылки, такие как ValueOf<T>,
выполняет обновления графа, подменяет
обновлённые подграфы в живое приложение и освобождает компоненты в правильном порядке.
Полезный способ резюмировать разделение:
время компиляции:
определить и валидировать граф приложения
рантайм:
исполнять и управлять этим графом
Это держит рантайм динамичным там, где состояние рантайма действительно имеет значение, не требуя от приложения повторного обнаружения собственной архитектуры при каждом запуске.
Приложение — это граф¶
Небольшое приложение Kora может выглядеть обманчиво простым:
Но этот исходный код уже кодирует существенный объём архитектуры.
@KoraApp определяет границу графа. Объявления @Component вводят провайдеров. Параметры конструкторов и фабрик определяют требования зависимостей. Теги уточняют идентичность. All<T> меняет
кардинальность. ValueOf<T> меняет связность жизненного цикла. @Root определяет активные точки входа графа. Lifecycle даёт узлам поведение запуска и завершения. Модули вносят дополнительных
провайдеров.
Расширения времени компиляции генерируют инфраструктуру, удовлетворяющую иначе неразрешённые зависимости.
Kora затем валидирует эти отношения и выдаёт результирующий граф приложения.
Полный процесс выглядит примерно так:
объявления в исходном коде
↓
обнаружить провайдеров
↓
идентифицировать корни графа
↓
разрешить требования зависимостей
↓
применить типы, теги, значения по умолчанию и кардинальность
↓
сгенерировать компоненты, предоставляемые фреймворком
↓
обнаружить недостающие, неоднозначные или циклические зависимости
↓
сгенерировать ApplicationGraph
↓
скомпилировать
↓
рантайм инициализирует и управляет графом
Традиционное внедрение зависимостей часто описывают как контейнер, который находит объекты за вас.
Модель Kora точнее этого.
Компилятор доказывает, как приложение может быть собрано, генерирует этот граф сборки, а рантайм его исполняет.
Это архитектурное различие за внедрением зависимостей времени компиляции в Kora.