Управление YAML-конфигурацией в Kora¶
Это руководство знакомит с типобезопасной конфигурацией в Kora и YAML. Оно показывает, как записи конфигурации извлекаются из application.yaml, как обязательные значения и значения по умолчанию
выражаются в Java-коде, и как переиспользуемые фрагменты конфигурации можно внедрять в несколько компонентов без дублирования всего блока. Также вы увидите, как переменные окружения и вывод значений
во время выполнения помогают легко проверять итоговую конфигурацию.
Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Java Config YAML App.
Если в процессе захочется сверить результат, используйте готовое рабочее приложение: Kora Kotlin Config YAML App.
Что вы создадите¶
Вы соберете небольшое запускаемое приложение Kora, которое:
- связывает
app.name,app.versionиapp.environmentчерез@ConfigSource - считает
APP_VERSIONобязательным значением, аAPP_NAMEпереопределением из окружения со значением по умолчанию - определяет один переиспользуемый
LibConfigсendpointиrequestTimeout - извлекает тот же
LibConfigдляlib1иlib2 - переиспользует один общий YAML-объект и переопределяет только одно поле для второй библиотеки
- печатает все итоговые значения в
stdoutво время запуска
Что потребуется¶
- JDK 17 или новее
- Gradle 7+
- редактор кода или среда разработки
- пройденное руководство Создание первого приложения на Kora
Требования¶
Требуется: завершить начальное руководство
Это руководство предполагает, что вы прошли Создание первого приложения на Kora и уже имеете запускаемый проект Kora с плагином application и сгенерированным графом приложения.
Если вы еще не создали такую базовую заготовку, сначала пройдите начальное руководство, потому что этот материал сосредоточен на типизированной конфигурации, а не на первоначальной настройке проекта.
Обзор¶
Конфигурация — это способ, которым окружения времени выполнения влияют на поведение приложения без изменения кода. Порты, учетные данные, переключатели возможностей, тайм-ауты и адреса внешних сервисов должны жить вне скомпилированных классов, но коду приложения все равно нужен типобезопасный способ читать эти значения.
Главный урок: конфигурация должна быть явной на границе приложения. Компоненты не должны сами искать переменные окружения или разбирать файлы; они должны получать типизированную конфигурацию из графа.
YAML и типобезопасное извлечение¶
Kora умеет читать конфигурацию YAML через SnakeYAML и извлекать ее в Java-интерфейсы или записи. Вместо передачи сырых строк и словарей через приложение, компоненты получают типизированные объекты конфигурации. Это делает обязательные значения явными и позволяет компилятору помогать при использовании конфигурации.
В этом руководстве используются два взаимодополняющих стиля отображения:
@ConfigSource("app")связывает одну фиксированную секцию конфигурации с типобезопасной зависимостью@ConfigValueExtractorописывает переиспользуемую форму конфигурации, которую можно извлекать из разных путей
Используйте @ConfigSource, когда компоненту нужна одна стабильная секция конфигурации приложения. Используйте @ConfigValueExtractor, когда одна и та же структура встречается в нескольких местах и
нужен один переиспользуемый извлекатель.
Обязательные и значения по умолчанию¶
YAML поддерживает полезные возможности композиции:
- обязательную подстановку из переменной окружения, например
${APP_VERSION} - подстановку из переменной окружения со значением по умолчанию, например
${APP_NAME:Task Management App} - переиспользование объекта через подстановки Kora из другой YAML-секции
Эти возможности помогают одному файлу конфигурации оставаться читаемым и при этом адаптироваться к локальной разработке, тестам и развернутым окружениям.
Как контракт protobuf в gRPC или контракт кеша в кешировании, тип конфигурации является контрактом границы. Он говорит, какие значения времени выполнения ожидает приложение и какую форму эти значения должны иметь.
Конфигурация как зависимость графа¶
В Kora конфигурация является частью графа зависимостей. Компонент может запросить типизированный объект конфигурации в конструкторе так же, как репозиторий или клиент. Это делает зависимости от конфигурации видимыми и тестируемыми. Также это удерживает разбор конфигурации на границе графа, а не размазывает его по коду приложения.
Практический поток:
- добавить модуль конфигурации YAML
- определить фиксированный источник конфигурации приложения
- связать обязательные значения и значения по умолчанию
- определить переиспользуемый извлекатель значений
- переиспользовать одну форму конфигурации для настроек нескольких библиотек
- запустить приложение и посмотреть итоговую конфигурацию
Зависимости¶
Добавьте модуль YAML в существующий проект и оставьте логирование включенным, чтобы поведение при запуске было видно во время обучения.
Обновите build.gradle:
Почему это важно:
config-yamlвключает загрузку YAML-файла в граф приложенияlogging-logbackделает запуск и диагностику неполадок видимыми, пока приложение работает
Модули¶
Начните с минимального графа приложения, который может загрузить YAML-конфигурацию и запустить приложение Kora.
На этом этапе мы еще не добавляем конфигурацию, специфичную для приложения. Мы только подготавливаем граф, чтобы на следующих шагах связать типизированную конфигурацию и напечатать итоговые значения.
Создайте src/main/java/ru/tinkoff/kora/guide/config/yaml/Application.java:
package ru.tinkoff.kora.guide.config.yaml;
import ru.tinkoff.kora.application.graph.KoraApplication;
import ru.tinkoff.kora.common.KoraApp;
import ru.tinkoff.kora.config.yaml.YamlConfigModule;
import ru.tinkoff.kora.logging.logback.LogbackModule;
@KoraApp
public interface Application extends
YamlConfigModule, // <----- Подключили модуль
LogbackModule {
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
Создайте src/main/kotlin/ru/tinkoff/kora/guide/config/yaml/Application.kt:
package ru.tinkoff.kora.guide.config.yaml
import ru.tinkoff.kora.application.graph.KoraApplication
import ru.tinkoff.kora.common.KoraApp
import ru.tinkoff.kora.config.yaml.YamlConfigModule
import ru.tinkoff.kora.logging.logback.LogbackModule
@KoraApp
interface Application :
YamlConfigModule, // <----- Подключили модуль
LogbackModule
fun main() {
KoraApplication.run(ApplicationGraph::graph)
}
Почему это важно:
YamlConfigModuleактивирует загрузку конфигурации в формате YAMLLogbackModuleподключает базовое логирование для запуска и диагностики- граф пока остается минимальным: он только умеет стартовать приложение и читать файл конфигурации
Типизированные секции появятся постепенно: сначала секция приложения, затем отдельная форма для библиотек и только после этого явное связывание путей libs.lib1 и libs.lib2 с двумя экземплярами одного типа.
Если хотите больше контекста о связывании графа и фабриках, смотрите документацию по контейнеру.
Конфигурация приложения¶
Теперь добавим первый типизированный контракт конфигурации: стабильную секцию приложения с именем app.
Это самый простой и самый частый шаблон конфигурации в Kora. Вместо ручного чтения ключей вы один раз объявляете форму и внедряете ее туда, где она нужна.
Создайте src/main/java/ru/tinkoff/kora/guide/config/yaml/AppConfig.java:
Почему это важно:
@ConfigSource("app")делает секциюappполноценной зависимостью- контракт остается рядом с кодом, который его использует
- рефакторинг ключей конфигурации становится безопаснее, потому что структура явно описана в одном месте
Обязательные значения¶
Когда AppConfig определен, можно решить, какие значения обязательны, а какие могут использовать значения по умолчанию.
Обновите src/main/resources/application.yaml:
app:
name: ${APP_NAME:Task Management App}
version: ${APP_VERSION}
environment: "development"
Что это означает:
version: ${APP_VERSION}является обязательным, поэтому запуск завершается ошибкой, еслиAPP_VERSIONотсутствуетname: ${APP_NAME:Task Management App}используетAPP_NAME, когда переменная существует, а иначе возвращается к значению по умолчаниюenvironmentостается обычным статическим значением, потому что в этом руководстве его пока не нужно менять
Это важный шаблон YAML: критически важные значения должны завершать запуск с ошибкой как можно раньше, а косметические или зависящие от окружения значения должны легко переопределяться.
Подробнее о правилах подстановки и поддерживаемых типах значений смотрите в документации по конфигурации.
Конфигурация библиотек¶
Теперь создадим переиспользуемую форму конфигурации для одной библиотеки.
Представим, что абстрактной библиотеке нужны две настройки:
endpointrequestTimeout
Вместо хранения этих значений как сырых ключей, опишите их один раз как тип.
Создайте src/main/java/ru/tinkoff/kora/guide/config/yaml/LibConfig.java:
Создайте src/main/kotlin/ru/tinkoff/kora/guide/config/yaml/LibConfig.kt:
Теперь, когда тип LibConfig уже объявлен, можно вернуться к графу приложения и явно показать, откуда берутся две библиотечные конфигурации.
@ConfigValueExtractor генерирует извлекатель для формы LibConfig, а методы графа выбирают конкретные ветки файла конфигурации. Так Kora получает два разных экземпляра одного типа: один для libs.lib1, второй для libs.lib2.
Обновите src/main/java/ru/tinkoff/kora/guide/config/yaml/Application.java:
package ru.tinkoff.kora.guide.config.yaml;
import ru.tinkoff.kora.application.graph.KoraApplication;
import ru.tinkoff.kora.common.KoraApp;
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.config.common.Config;
import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor;
import ru.tinkoff.kora.config.yaml.YamlConfigModule;
import ru.tinkoff.kora.logging.logback.LogbackModule;
@KoraApp
public interface Application extends
YamlConfigModule, // <----- Подключили модуль
LogbackModule {
final class Lib1Tag {
private Lib1Tag() {}
}
final class Lib2Tag {
private Lib2Tag() {}
}
@Tag(Lib1Tag.class)
default LibConfig lib1Config(Config config, ConfigValueExtractor<LibConfig> extractor) {
return extractor.extract(config.get("libs.lib1"));
}
@Tag(Lib2Tag.class)
default LibConfig lib2Config(Config config, ConfigValueExtractor<LibConfig> extractor) {
return extractor.extract(config.get("libs.lib2"));
}
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph);
}
}
Обновите src/main/kotlin/ru/tinkoff/kora/guide/config/yaml/Application.kt:
package ru.tinkoff.kora.guide.config.yaml
import ru.tinkoff.kora.application.graph.KoraApplication
import ru.tinkoff.kora.common.KoraApp
import ru.tinkoff.kora.common.Tag
import ru.tinkoff.kora.config.common.Config
import ru.tinkoff.kora.config.common.extractor.ConfigValueExtractor
import ru.tinkoff.kora.config.yaml.YamlConfigModule
import ru.tinkoff.kora.logging.logback.LogbackModule
@KoraApp
interface Application :
YamlConfigModule, // <----- Подключили модуль
LogbackModule {
class Lib1Tag private constructor()
class Lib2Tag private constructor()
@Tag(Lib1Tag::class)
fun lib1Config(config: Config, extractor: ConfigValueExtractor<LibConfig>): LibConfig {
return extractor.extract(config.get("libs.lib1"))
}
@Tag(Lib2Tag::class)
fun lib2Config(config: Config, extractor: ConfigValueExtractor<LibConfig>): LibConfig {
return extractor.extract(config.get("libs.lib2"))
}
}
fun main() {
KoraApplication.run(ApplicationGraph::graph)
}
Что здесь происходит:
Lib1TagиLib2Tagразличают два экземпляраLibConfigв графеconfig.get("libs.lib1")иconfig.get("libs.lib2")выбирают разные ветки конфигурацииConfigValueExtractor<LibConfig>преобразует каждую ветку в типизированный объект
Добавьте первую секцию библиотеки в application.yaml:
app:
name: ${APP_NAME:Task Management App}
version: ${APP_VERSION}
environment: "development"
libs:
lib1:
endpoint: "https://integration.local/api"
requestTimeout: "5s"
На этом этапе LibConfig используется только для lib1. Граф приложения извлекает его из libs.lib1, а Kora напрямую преобразует "5s" в Duration.
Файл конфигурации¶
Теперь представим, что второй библиотеке нужна ровно такая же форма.
Можно продублировать весь блок конфигурации, но YAML вместе с подстановками Kora дает лучший вариант: положить общие скалярные значения в одну секцию и ссылаться на эти значения там, где нужно.
Снова обновите application.yaml:
app:
name: ${APP_NAME:Task Management App}
version: ${APP_VERSION}
environment: "development"
commonLib:
endpoint: "https://integration.local/api"
requestTimeout: "5s"
libs:
lib1:
endpoint: ${commonLib.endpoint}
requestTimeout: ${commonLib.requestTimeout}
lib2:
endpoint: "https://integration-2.local/api"
requestTimeout: ${commonLib.requestTimeout}
Что изменилось:
commonLibхранит общие скалярные значения один разlibs.lib1ссылается на оба общих значенияlibs.lib2переопределяет толькоendpointlibs.lib2.requestTimeoutвсе еще переиспользует общий тайм-аут
В этом и есть выигрыш от сочетания переиспользования YAML с @ConfigValueExtractor: одна форма конфигурации, несколько извлеченных экземпляров, минимум дублирования.
Итоговые значения¶
Последний шаг — доказать, что все внедрилось корректно.
Вместо HTTP-маршрута это руководство использует маленький компонент @Root, который печатает все итоговые значения в стандартный вывод во время запуска. Это похоже на проверку через консоль из руководства по
внедрению зависимостей.
Создайте src/main/java/ru/tinkoff/kora/guide/config/yaml/ConfigRunner.java:
package ru.tinkoff.kora.guide.config.yaml;
import java.util.LinkedHashMap;
import java.util.Map;
import ru.tinkoff.kora.application.graph.Lifecycle;
import ru.tinkoff.kora.common.Component;
import ru.tinkoff.kora.common.Tag;
import ru.tinkoff.kora.common.annotation.Root;
@Root
@Component
public final class ConfigRunner implements Lifecycle {
private final AppConfig appConfig;
private final LibConfig lib1Config;
private final LibConfig lib2Config;
public ConfigRunner(
AppConfig appConfig,
@Tag(Application.Lib1Tag.class) LibConfig lib1Config,
@Tag(Application.Lib2Tag.class) LibConfig lib2Config
) {
this.appConfig = appConfig;
this.lib1Config = lib1Config;
this.lib2Config = lib2Config;
}
public Map<String, String> snapshot() {
Map<String, String> values = new LinkedHashMap<>();
values.put("name", this.appConfig.name());
values.put("version", this.appConfig.version());
values.put("environment", this.appConfig.environment());
values.put("lib1.endpoint", this.lib1Config.endpoint());
values.put("lib1.requestTimeout", this.lib1Config.requestTimeout().toString());
values.put("lib2.endpoint", this.lib2Config.endpoint());
values.put("lib2.requestTimeout", this.lib2Config.requestTimeout().toString());
return values;
}
@Override
public void init() {
System.out.println("Config guide start");
this.snapshot().forEach((key, value) -> System.out.println(key + "=" + value));
}
@Override
public void release() {
System.out.println("Application shutdown");
}
}
Создайте src/main/kotlin/ru/tinkoff/kora/guide/config/yaml/ConfigRunner.kt:
package ru.tinkoff.kora.guide.config.yaml
import ru.tinkoff.kora.application.graph.Lifecycle
import ru.tinkoff.kora.common.Component
import ru.tinkoff.kora.common.Tag
import ru.tinkoff.kora.common.annotation.Root
@Root
@Component
class ConfigRunner(
private val appConfig: AppConfig,
@Tag(Application.Lib1Tag::class) private val lib1Config: LibConfig,
@Tag(Application.Lib2Tag::class) private val lib2Config: LibConfig,
) : Lifecycle {
fun snapshot(): Map<String, String> {
return linkedMapOf(
"name" to appConfig.name(),
"version" to appConfig.version(),
"environment" to appConfig.environment(),
"lib1.endpoint" to lib1Config.endpoint(),
"lib1.requestTimeout" to lib1Config.requestTimeout().toString(),
"lib2.endpoint" to lib2Config.endpoint(),
"lib2.requestTimeout" to lib2Config.requestTimeout().toString(),
)
}
override fun init() {
println("Config guide start")
snapshot().forEach { (key, value) -> println("$key=$value") }
}
override fun release() {
println("Application shutdown")
}
}
Почему это важно:
@Rootгарантирует, что запускаемый компонент действительно будет создан при старте приложенияLifecycleдает естественное место для печати или проверки внедренных значенийsnapshot()удерживает вывод во время выполнения и тесты вокруг одного контракта
Запуск приложения¶
Используйте стандартную последовательность действий из руководств:
В запускаемом примере задача run внедряет APP_VERSION из koraVersion в gradle.properties, поэтому обычный ./gradlew run работает из коробки.
Если хотите также переопределить имя приложения, добавьте APP_NAME перед запуском:
Вывод приложения¶
Когда приложение стартует, оно должно напечатать примерно такой вывод:
Config guide start
name=Task Management App
version=1.0.0
environment=development
lib1.endpoint=https://integration.local/api
lib1.requestTimeout=PT5S
lib2.endpoint=https://integration-2.local/api
lib2.requestTimeout=PT5S
Если вы передадите APP_NAME, напечатанная строка name= должна показать переопределение.
Вторая конфигурация¶
Частый следующий шаг — держать отдельные файлы конфигурации для разных окружений, например разработки, стенда или промышленного окружения.
Например, создайте src/main/resources/application-prod.yaml:
app:
name: ${APP_NAME:Task Management App}
version: ${APP_VERSION}
environment: "production"
commonLib:
endpoint: "https://integration.local/api"
requestTimeout: "5s"
libs:
lib1:
endpoint: ${commonLib.endpoint}
requestTimeout: ${commonLib.requestTimeout}
lib2:
endpoint: "https://integration-2.local/api"
requestTimeout: ${commonLib.requestTimeout}
Этот файл сохраняет ту же форму, что и application.yaml, и меняет только значение окружения. YAML-конфигурация выбирается через свойство Kora config.resource, поэтому альтернативный ресурс должен
быть достаточно полным, чтобы приложение могло стартовать самостоятельно.
Запустить приложение с этой конфигурацией можно через системное свойство Kora config.resource:
С таким переопределением вывод при запуске должен напечатать:
Подробнее о поиске файлов и внешних файлах конфигурации смотрите в документации по конфигурации.
Лучшие практики¶
- Используйте
@ConfigSourceдля стабильной конфигурации уровня приложения, которая принадлежит одной хорошо известной секции. - Используйте
@ConfigValueExtractor, когда одна форма конфигурации переиспользуется под несколькими путями. - Держите обязательные значения явными через
${VAR_NAME}, а значения по умолчанию явными через${VAR_NAME:default}. - Предпочитайте общие YAML-секции плюс подстановки Kora вместо копирования одинаковых скалярных значений в несколько секций.
- Пока изучаете поведение конфигурации, держите диагностику запуска простой;
System.out.println(...)достаточно для учебной последовательности действий.
Итоги¶
Теперь у вас есть рабочее приложение Kora на основе YAML, которое связывает конфигурацию двумя способами. AppConfig отображает стабильную секцию app, а LibConfig дважды извлекается из двух
разных путей с разными метками. Переиспользование YAML делает файл компактным, а одно переопределение меняет только endpoint второй библиотеки.
Ключевые понятия¶
@ConfigSource:
- отображает одну фиксированную секцию конфигурации на типобезопасный интерфейс
- хорошо подходит для настроек приложения вроде
app.nameиapp.environment
Обязательные значения и значения по умолчанию:
${APP_VERSION}является обязательным и завершает запуск с ошибкой как можно раньше, если значение отсутствует${APP_NAME:Task Management App}использует значение из окружения, когда оно присутствует, а иначе возвращается к настроенному значению по умолчанию
@ConfigValueExtractor и переиспользование:
- одну форму конфигурации можно извлекать из нескольких путей
- общая секция вроде
commonLibможет один раз хранить скалярные значения по умолчанию - подстановки вроде
${commonLib.requestTimeout}переиспользуют эти скалярные значения в нескольких типизированных секциях конфигурации
Устранение неполадок¶
Приложение падает при старте из-за неразрешенной подстановки:
app.version: ${APP_VERSION} является обязательным. В запускаемом примере задача run предоставляет его автоматически из koraVersion. Если вы удалите это связывание Gradle, нужно
задать APP_VERSION перед запуском.
APP_NAME не меняет имя по умолчанию:
Используйте форму подстановки со значением по умолчанию:
Значения конфигурации библиотеки дублируются между секциями:
Перенесите общие скалярные значения в секцию вроде commonLib и ссылайтесь на них через подстановки вида ${commonLib.requestTimeout} вместо копирования одного и того же значения в обе библиотеки.
Сборка зависает или падает неожиданно:
Остановите демоны Gradle и повторите:
AccessDeniedException в кеше Gradle на Windows:
Если кешированные файлы заблокированы другим процессом, повторите запуск со свежим кешем текущего запуска:
Что дальше?¶
- Конфигурация HOCON, чтобы сравнить YAML с распространенной для Kora настройкой на основе HOCON.
- Работа с JSON, чтобы сделать DTO запросов и ответов явными в небольшом приложении, которое у вас уже есть.
- Создание HTTP-сервера после JSON, потому что это руководство опирается на отображение JSON DTO и превращает приложение в более полноценный HTTP API.
- Изучите основы внедрения зависимостей, если сгенерированный граф и фабрики конфигурации все еще кажутся неочевидными.
Помощь¶
Если возникли проблемы:
- сравните с Kora Java Config YAML App и Kora Kotlin Config YAML App
- проверьте документацию по конфигурации
- проверьте документацию по контейнеру
- прочитайте спецификацию YAML