Контейнер
Контейнер зависимостей — это ядро фреймворка Kora. Он строит граф зависимостей, проверяет его,
внедряет компоненты, инициализирует их и позднее освобождает.
В отличие от контейнеров, которые собирают приложение при запуске путем сканирования classpath, Kora строит большую часть графа
во время компиляции и генерирует обычный Java-код для запуска приложения.
Работа контейнера в Kora разделена на две части: время компиляции и время выполнения.
Во время компиляции Kora проверяет, что все зависимости могут быть найдены и связаны. Во время выполнения контейнер создает
компоненты, управляет их жизненным циклом и обновляет затронутые части графа при возникновении изменений.
Пошаговый разбор перед справочным описанием смотрите в разделах Введение во внедрение зависимостей и Внедрение зависимостей.
Время компиляции¶
Во время компиляции компоненты обнаруживаются, чтобы построить контейнер зависимостей для всего приложения. Это позволяет проверить контейнер зависимостей до фактического запуска приложения.
Контейнер¶
Ядром контейнера зависимостей является интерфейс, помеченный аннотацией @KoraApp.
Эту аннотацию следует использовать на интерфейсе, который содержит фабричные методы для создания компонентов
и подключает внешние модули.
В приложении может быть только один такой интерфейс.
Обработчики аннотаций Kora анализируют исходный код в модуле компиляции, где объявлен @KoraApp,
а также в модулях, где объявлен @KoraSubmodule. Обычные модули проекта без
@KoraApp или @KoraSubmodule не становятся областями обнаружения компонентов автоматически.
Компоненты¶
Компонент — это зависимость в контейнере зависимостей.
Все компоненты в Kora создаются в единственном экземпляре (Singleton).
Компоненты внедряются только если они являются корневыми компонентами или если другие компоненты нуждаются в них как в зависимостях.
Компоненты, которые не удовлетворяют этим требованиям, не включаются в контейнер зависимостей.
Автоматическая фабрика¶
Аннотация @Component помечает класс как доступный через контейнер. К классу предъявляются следующие требования:
- Класс не должен быть абстрактным
- У класса должен быть только один публичный конструктор
- Класс должен быть
final(только если у него нет аспектов)
Фабрика метод¶
Фабричный метод — это метод с модификатором default, который возвращает компонент.
Метод может принимать другие компоненты-зависимости в качестве аргументов.
Контейнер зависимостей ниже описывает две фабрики, где фабрика otherService требует компонент, созданный фабрикой someService.
Это самый базовый способ, которым компоненты могут быть зарегистрированы в контейнере:
Фабричный метод не должен предоставлять значение null в качестве компонента.
Фабрика модуль¶
Компоненты для контейнера зависимостей также могут располагаться в модулях внутри проекта приложения.
Модуль — это интерфейс, который содержит фабричные методы.
Аннотация @Module помечает интерфейс как модуль, который должен быть внедрен в контейнер приложения во время компиляции.
Модуль должен находиться в той же директории исходного кода, что и класс, помеченный @KoraApp.
Все фабричные методы внутри модуля становятся доступны контейнеру зависимостей:
Фабрика внешний модуль¶
Компоненты для контейнера зависимостей также могут быть найдены во внешних модулях из сторонних зависимостей.
Модуль — это интерфейс, который содержит фабричные методы.
Kora не выполняет автоматический поиск модулей из внешних зависимостей, как это делают некоторые другие DI-решения.
Это позволяет разработчику точно контролировать, какие зависимости используются в приложении, и избегать
инициализации ненужных компонентов.
Все необходимые внешние модули из зависимостей должны быть подключены явно в интерфейсе, помеченном аннотацией @KoraApp, через наследование:
Такой модуль может быть объявлен в любом интерфейсе: в сторонней библиотеке, в отдельном модуле проекта или рядом с самим @KoraApp.
Важно то, что интерфейс @KoraApp явно подключает его через наследование.
Фабрика подмодуль¶
Аннотация @KoraSubmodule помечает интерфейс, для которого должен быть построен модуль в рамках текущего модуля компиляции.
Он будет содержать все компоненты, помеченные аннотациями @Module и @Component.
Эта аннотация полезна, когда вы разбиваете проект на многомодульное приложение
с точки зрения инструмента сборки Gradle, где каждый модуль отвечает за свою часть функциональности,
а приложение с @KoraApp собирается в отдельном модуле компиляции.
Такой подход помогает структурировать большой проект по доменным областям и улучшить время сборки:
изменения в одном модуле проекта не заставляют обработчик аннотаций заново анализировать весь код приложения.
Для интерфейса будет сгенерирован интерфейс-наследник. Он унаследует все интерфейсы, помеченные @Module,
и создаст фабричные методы для классов, помеченных как @Component.
Например, у вас есть отдельный модуль приложения, который содержит такой @KoraSubmodule:
И есть основной модуль приложения с точкой сборки для всего приложения:
Это подключит модуль SomeModule, найденный через SomeSubModule, к итоговому контейнеру приложения.
Распространенный практический сценарий использования @KoraSubmodule — это отдельный модуль Gradle, который владеет одной доменной областью и просто
агрегирует нужные ему внешние модули (базы данных, кэши и так далее) путем их наследования,
вместе со своими собственными классами @Component и интерфейсами @Module. Модуль приложения затем подключает
сгенерированные подмодули так же, как подключает любой другой модуль:
Фабрика обобщенная¶
Если контейнер зависимостей не смог найти фабрику для конкретного типа, контейнер Kora может попытаться найти
методы с обобщенными параметрами во время компиляции и использовать такой метод для создания экземпляра требуемого класса.
Теперь, если какому-либо компоненту нужен GenericValidator в качестве зависимости, для его создания будет использована эта фабрика.
Информация об обобщенном типе¶
Если фабричному методу нужно знать точный обобщенный тип, запрашиваемый контейнером в данный момент, он может внедрить TypeRef<T>.
Это полезно для инфраструктурных компонентов, которые создают зависимость по форме типа, а не только по «сырому» классу.
TypeRef<T> переносит информацию об обобщенном типе через стирание типов в Java. Большинству компонентов приложения он не нужен,
но он полезен для универсальных фабрик, мапперов и расширений контейнера.
Механизм расширений¶
Если ни одна из фабрик не смогла предоставить компонент, Kora может попытаться создать эту зависимость самостоятельно во время компиляции.
Для этого предоставляется механизм расширений. Каждое расширение способно сообщить, может ли оно создать компонент требуемого типа.
Если расширение может это сделать, оно выполняет необходимую генерацию кода и сообщает, как получить этот компонент.
Например, существуют расширения, которые умеют создавать оптимальные компоненты JsonReader и JsonWriter, репозитории и другие компоненты.
Доступные расширения обнаруживаются через механизм ServiceLocator из всех зависимостей, предоставленных в области видимости обработчика аннотаций.
Этот механизм является системным и чаще всего используется внутренними модулями Kora.
Фабрика по умолчанию¶
Чтобы предоставить компоненты по умолчанию через фабричные методы, которые, как предполагается, пользователь может переопределить,
требуется использовать аннотацию @DefaultComponent.
Если контейнер зависимостей находит во время компиляции любой компонент того же типа и с теми же тегами, но без @DefaultComponent,
то при внедрении будет предпочтен пользовательский компонент.
Автоматическое создание¶
Если ни один из вышеперечисленных методов не смог предоставить компонент,
то Kora может попытаться создать компонент самостоятельно, если он удовлетворяет требованиям, аналогичным автоматической фабрике:
- Класс не должен быть абстрактным
- У класса должен быть только один публичный конструктор
- Класс должен быть
final(только если у него нет аспектов)
public final class SomeService {
private OtherService otherService;
public SomeService(OtherService otherService) {
this.otherService = otherService;
}
}
@KoraApp
public interface Application {
default SomeOtherService someOtherService(SomeService someService) {
return new SomeOtherService(someService);
}
default OtherService otherService() {
return new OtherService();
}
}
- Класс не должен быть абстрактным
- У класса должен быть только один публичный конструктор
- Класс не должен быть
open(только если у него нет аспектов)
Переопределение компонента¶
В случае, если компонент предоставляется библиотекой как зависимость по умолчанию,
можно создать фабрику в приложении без аннотации @DefaultComponent, и такая зависимость переопределит его.
Поскольку все внешние модули подключаются как интерфейсы к ядру контейнера @KoraApp и их фабрики доступны,
вы можете просто переопределить их как метод и предоставить свою собственную реализацию.
Корневой компонент¶
Когда требуется, чтобы компонент всегда инициализировался при запуске приложения, даже если он не является зависимостью других компонентов,
предполагается использовать аннотацию @Root над фабричным методом или классом, помеченным @Component.
Примером такого компонента может быть HTTP-сервер, потребитель Kafka, компонент прогрева кэша или обработчик выполняемой фоновой задачи.
Необязательные зависимости¶
Если вы хотите ввести необязательную зависимость, которой может не существовать, то
предполагается пометить такой компонент любой аннотацией @Nullable,
тогда контейнер зависимостей не завершится с ошибкой во время компиляции из-за отсутствия компонента:
@Component
public final class SomeService {
private OtherService otherService;
public SomeService(@Nullable OtherService otherService) { //(1)!
this.otherService = otherService;
}
}
- Подойдет любая аннотация
@Nullable, напримерjavax.annotation.Nullable/jakarta.annotation.Nullable/org.jetbrains.annotations.Nullable.
Если вы хотите внедрить необязательную зависимость, которая может отсутствовать, используйте синтаксис null-безопасности Kotlin
и пометьте этот компонент как допускающий null,
тогда контейнер зависимостей не завершится с ошибкой во время компиляции из-за отсутствия компонента:
Необязательность можно комбинировать с обертками контейнера: ValueOf<Optional<T>>, Optional<ValueOf<T>>,
PromiseOf<Optional<T>> и Optional<PromiseOf<T>>. Это полезно, когда зависимость может отсутствовать,
но компоненту при этом все равно нужен отложенный доступ или возможность обновить ее через контейнер.
Список компонентов¶
В контейнере может быть много экземпляров одного и того же типа, и если вы хотите собрать их все в одном месте, следует использовать специальный тип All.
Например, у нас есть некоторая сущность Handler, и она внедряется N различными типами в контейнере.
SomeProcessor при этом потребляет все возможные реализации этого типа.
Важно: пример выше берет все экземпляры Handler без тегов.
Сам тип All имеет следующий контракт:
Это токен-тип, который расширяет List и может быть передан в конструкторы, ожидающие List.
Если вам нужно собрать ссылки на компоненты вместо самих компонентов, контейнер также поддерживает
All<ValueOf<T>> и All<PromiseOf<T>>.
Теги¶
Иногда возникает необходимость предоставить разные экземпляры одного и того же типа разным компонентам. Для этой цели их можно различать по тегам.
Для этого существует аннотация @Tag, которая принимает на вход класс тега.
Ожидается сопоставление, при котором компонент регистрируется с определенным тегом, а в точке внедрения объявляется с точно таким же тегом.
Используется именно класс, а не строковый литерал, потому что так проще ориентироваться в коде и проще выполнять рефакторинг.
Вот как можно внедрять разные экземпляры класса с общим интерфейсом в разные точки внедрения:
public interface SomeModule {
@Tag(MyTag1.class)
default SomeService someService1() {
return new SomeService1();
}
@Tag(MyTag2.class)
default SomeService someService2() {
return new SomeService2();
}
default ServiceC serviceA(@Tag(MyTag1.class) SomeService service) {
return new ServiceA(service);
}
default ServiceD serviceB(@Tag(MyTag2.class) SomeService service) {
return new ServiceB(service);
}
}
interface SomeModule {
@Tag(MyTag1::class)
fun someService1(): SomeService = SomeService1()
@Tag(MyTag2::class)
fun someService2(): SomeService = SomeService2()
fun serviceA(@Tag(MyTag1::class) service: SomeService): ServiceA {
return ServiceA(service)
}
fun serviceB(@Tag(MyTag2::class) service: SomeService): ServiceB {
return ServiceB(service)
}
}
Теги над методом сообщают, с каким тегом зарегистрировать компонент, а теги в точках внедрения сообщают, какой помеченный тегом компонент ожидать.
Теги также работают на параметрах конструктора, в сочетании с @Component или финальными классами.
@Tag(MyTag1.class)
class SomeService1 implements SomeService {
}
@Tag(MyTag2.class)
final class SomeService2 implements SomeService {
}
final class ServiceA {
private final SomeService service;
public ServiceA(@Tag(MyTag1.class) SomeService service) {
this.service = service;
}
}
final class ServiceB {
private final SomeService service;
public ServiceB(@Tag(MyTag2.class) SomeService service) {
this.service = service;
}
}
Собственный тег¶
Вы также можете создавать свои собственные аннотации-теги и работать с ними. Одним из примеров является аннотация @Json.
Все по тегу¶
Вы также можете использовать тег, чтобы получить список всех компонентов по определенному тегу:
Любой тег¶
Чтобы получить список всех компонентов с тегом и без него, нужно использовать специальный тип тега @Tag.Any:
Циклические зависимости¶
Поскольку Kora строит и проверяет весь граф зависимостей во время компиляции, цикл зависимостей
(компоненту A нужен B, компоненту B нужен A, возможно, через несколько компонентов между ними) обнаруживается во время компиляции,
а не приводит к сбою во время выполнения. То, как обрабатывается такой цикл, зависит от того, как объявлена зависимость внутри цикла.
Прямая зависимость от final-класса (или любого не-интерфейсного типа).
Такой цикл не может быть разрешен, и компиляция завершается ошибкой. Ошибка указывает на тип, который замыкает цикл, и перечисляет
кандидатов цикла:
Encountered circular dependency in graph for source type: ru.tinkoff.kora.example.ServiceA (no tags)
Cycle dependency candidates:
- ru.tinkoff.kora.example.ServiceA
- ru.tinkoff.kora.example.ServiceB
Please check that you are not using cycle dependency in ru.tinkoff.kora.application.graph.Lifecycle, this is forbidden.
Зависимость, объявленная через интерфейс (или не-final-класс).
Kora разрывает цикл автоматически: для зависимости с типом-интерфейсом она генерирует ленивый прокси, реализующий
ru.tinkoff.kora.common.PromisedProxy<T>, и внедряет прокси вместо реального компонента. Прокси разрешает
фактический компонент из графа при первом обращении (и повторно разрешает его после обновления графа), так что оба компонента могут быть
сконструированы. От разработчика никаких действий не требуется, но имейте в виду, что проксируемая сторона становится пригодной к использованию только после
того, как граф полностью связан, поэтому ее нельзя вызывать из конструктора.
В примере ниже ServiceAImpl и ServiceBImpl ссылаются друг на друга через интерфейсы, поэтому цикл разрывается
автоматически сгенерированным PromisedProxy, и граф разрешается успешно:
Надежный способ намеренно разорвать цикл — внедрить одну из сторон через ValueOf<T>
или PromiseOf<T> вместо прямой зависимости. Это отвязывает потребителя от жизненного цикла другого
компонента, поэтому контейнер больше не рассматривает эти два компонента как жесткий цикл:
Время выполнения¶
Контейнер зависимостей использует максимально возможный параллелизм в рамках построенного графа.
Во время выполнения приложения контейнер делает следующее:
- Инициализирует все компоненты в контейнере зависимостей
- Отслеживает изменения в контейнере зависимостей
- Атомарно обновляет контейнер зависимостей при внесении изменений
- Выполняет штатное завершение при получении сигнала
SIGTERM
Все компоненты используют энергичную инициализацию, что означает, что они инициализируются сразу при запуске приложения.
Точка входа¶
Точка входа приложения должна вызывать KoraApplication.run, используя контейнер зависимостей, созданный во время компиляции.
Если интерфейс, помеченный @KoraApp, называется Application, то во время компиляции в том же пакете будет сгенерирован класс с именем ApplicationGraph.
Он представляет собой реализацию контейнера зависимостей, а точка входа
в том же пакете будет выглядеть так:
KoraApplication.run запускает контейнер и возвращает RefreshableGraph (Graph в сочетании с Lifecycle).
Для работающего приложения вы обычно не взаимодействуете с ним напрямую, но он полезен в тестах и продвинутых сценариях, где
нужно найти компонент в графе или вручную инициировать обновление.
Жизненный цикл контейнера¶
Контейнер зависимостей умеет инициализировать все компоненты в правильном порядке и делает это максимально параллельно, чтобы достичь максимально быстрого времени запуска.
Когда контейнер больше не нужен, он запускает механизм освобождения компонентов в обратном порядке.
В середине жизненного цикла компонент может быть обновлен, и тогда контейнер обновляет все компоненты, которые зависят от измененного компонента. Это происходит атомарно: в начале процесса открывается транзакция, которая закрывается только если все компоненты успешно инициализированы, и откатывается, если возникает хотя бы одна ошибка.
Жизненный цикл компонента¶
По умолчанию все компоненты создаются как синглтоны через конструктор или фабричный метод во время инициализации.
Если вам нужно выполнить какие-либо действия при инициализации компонента или перед его освобождением, вы должны реализовать интерфейс Lifecycle:
В контейнере зависимостей все компоненты инициализируются асинхронно и максимально параллельно.
Если вам нужно предоставить компонент с жизненным циклом из фабричного метода, вы можете использовать класс LifecycleWrapper.
Он реализует сразу два контракта:
Lifecycle— контейнер вызоветinit()при запуске иrelease()при освобождении компонентаWrapped<T>— контейнер внедрит значениеT, возвращаемое методомvalue()
Если вам нужно вернуть собственную обертку, она должна реализовывать Wrapped<T>:
Штатное завершение¶
Все интеграции, которые предоставляет Kora, такие как HTTP-сервер и потребитель Kafka,
поддерживают штатное завершение из коробки, используя
жизненный цикл компонента.
Все компоненты, которые реализуют AutoCloseable, также будут автоматически закрыты контейнером зависимостей перед освобождением.
Косвенная зависимость¶
Рассмотрим следующий пример:
У нас есть два сервиса и третий сервис, который зависит от них. Но есть разница в жизненном цикле.
Если мы берем тип как зависимость напрямую, то мы сообщаем контейнеру, что при обновлении компонента ServiceA нам нужно точно так же обновить компонент ServiceC.
Но когда мы используем обертку типа ValueOf, мы сообщаем контейнеру,
что ServiceC не связан с жизненным циклом ServiceB, и если ServiceB изменяется, то ServiceC обновлять не нужно.
Обновление компонентов¶
Обновление компонента возможно, если для внедрения зависимости используется обертка ValueOf:
Метод get() возвращает текущее состояние компонента в контейнере.
Этот механизм используется в компонентах, которые не могут быть перезагружены во время работы приложения.
Например, это касается различных серверов, которые слушают сокеты (HTTP, gRPC): обработчики запросов, которые могут изменяться,
поставляются им через ValueOf.
С помощью метода refresh() вы можете инициировать обновление компонента. Этот механизм используется, например, компонентом,
который отслеживает изменения файла конфигурации на диске.
Когда содержимое файла изменяется, он инициирует обновление компонента конфигурации, и затем все изменения распространяются
по цепочке компонентов, связанных прямыми зависимостями.
ValueOf также имеет дополнительные методы для удобной работы с обернутым значением:
map(...)— преобразует значение внутриValueOfбез изменения связи с исходным компонентомoptional()— преобразуетValueOf<T>вValueOf<Optional<T>>
Если компоненту нужна отложенная ссылка, он может использовать PromiseOf<T>.
Метод get() возвращает Optional<T>: до связывания графа он пуст, а после связывания получает текущий компонент из контейнера.
Как и ValueOf, PromiseOf поддерживает map(...) и optional().
Большинству бизнес-кода нужна только прямая зависимость или ValueOf; PromiseOf предназначен для более низкоуровневых сценариев,
где компоненту нужен отложенный доступ к другой части графа.
Если компонент, полученный через ValueOf<Wrapped<T>>, нужно передать дальше как обычный ValueOf<T>,
вы можете использовать Wrapped.UnwrappedValue.unwrap(...). Это полезно для оберток, которые добавляют жизненный цикл или другое поведение,
но должны предоставлять наружу обычное значение.
Слушатели обновлений¶
Если компоненту нужно знать, что граф был успешно обновлен, он может реализовать RefreshListener:
Контейнер вызывает graphRefreshed() после успешного обновления графа. Если компонент одновременно является оберткой значения и слушателем обновлений,
он может реализовать комбинированный интерфейс WrappedRefreshListener<T>.
RefreshListener нужен только для получения уведомления после завершения обновления. Он не требуется для того, чтобы контейнер
пересоздал компонент. Если обновление затрагивает компонент или его зависимости, а другие компоненты внедрили его напрямую,
без ValueOf или PromiseOf, то эти зависимые компоненты также будут обновлены автоматически.
Инспекция компонента¶
Бывают ситуации, когда компонент в контейнере нужно дополнительно изменить или инициализировать,
но никто не должен начинать работать с этим компонентом до завершения этих действий.
Для этого случая существует механизм перехвата компонентов. Поместите в контейнер объект, реализующий интерфейс GraphInterceptor.
Например, этот механизм можно использовать для прогрева кэша на основе JdbcDatabase:
Интерфейс GraphInterceptor почти такой же, как контракт Lifecycle, за исключением возвращаемого типа.
Метод init(T value) получает уже полностью инициализированный компонент. Метод может вернуть измененный или совершенно другой
экземпляр данного типа, и этот объект будет использован как зависимость другими компонентами.
Метод release(T value) получает компонент перед освобождением, то есть это все еще работающий и еще не очищенный экземпляр.