Конфигурация
Модуль конфигурации читает настройки приложения из файлов HOCON или YAML, переменных окружения, системных свойств
Java и отображает их на типизированные классы в Kora. Полученные объекты конфигурации становятся обычными
компонентами графа зависимостей и могут внедряться в сервисы, клиенты, серверы и другие интеграции.
В Kora конфигурация приложения обычно описывается интерфейсом с аннотацией @ConfigSource: путь в файле указывает на
читаемую секцию, а методы интерфейса описывают обязательные значения, необязательные значения и значения по умолчанию.
Библиотеки и переиспользуемые формы конфигурации используют @ConfigValueExtractor, который создает только правило
извлечения, тогда как конкретный путь выбирается в модуле библиотеки.
Для пошагового разбора перед справочным описанием смотрите Конфигурация HOCON и Конфигурация YAML.
HOCON¶
Поддержка HOCON реализована с помощью Typesafe Config.
HOCON — это формат конфигурационных файлов на основе JSON. Он менее строгий, чем JSON, и поддерживает подстановки,
значения по умолчанию и удобный синтаксис для вложенных объектов.
services {
foo {
bar = "SomeValue" //(1)!
baz = 10 //(2)!
propRequired = ${REQUIRED_ENV_VALUE} //(3)!
propOptional = ${?OPTIONAL_ENV_VALUE} //(4)!
propDefault = 10
propDefault = ${?NON_DEFAULT_ENV_VALUE} //(5)!
propReference = ${services.foo.bar}Other${services.foo.baz} //(6)!
propArray = ["v1", "v2"] //(7)!
propArrayAsString = "v1, v2" //(8)!
propMap = { //(9)!
"k1" = "v1"
"k2" = "v2"
}
propObject = { //(10)!
p1 = "v1"
p2 = "v2"
}
propObjects = [ //(11)!
{
p1 = "v1"
p2 = "v2"
},
{
p1 = "v3"
p2 = "v4"
}
]
}
}
- Строковое значение конфигурации
- Числовое значение конфигурации
- Обязательное значение конфигурации, подставляемое из переменной окружения
REQUIRED_ENV_VALUE - Необязательное значение конфигурации, подставляемое из переменной окружения
OPTIONAL_ENV_VALUE; если переменная не найдена, значение конфигурации опускается - Значение конфигурации со значением по умолчанию: значение по умолчанию задается как
propDefault = 10, аNON_DEFAULT_ENV_VALUE, если найдено, заменяет его - Значение конфигурации, собранное из подстановок других частей конфигурации со значением
Otherмежду ними - Значение конфигурации в виде списка строк; значение можно задать как массив строк или как строку с разделителем-запятой
- Значение конфигурации в виде списка строк; значение можно задать как строку с разделителем-запятой или как массив строк
- Значение конфигурации в виде словаря ключ-значение
- Значение конфигурации в виде отображаемого класса
- Значение конфигурации в виде списка отображаемых классов
Значения также могут ссылаться на другие ключи конфигурации (само-ссылка / перекрестная ссылка) через ${path},
а на переменные окружения — через ${VAR} (обязательные), ${?VAR} (необязательные) или через резервный вариант по
умолчанию. Все подстановки разрешаются после слияния каждого слоя, поэтому ссылка может указывать на ключ, определенный
в другом файле или в другом слое конфигурации.
Представление конфигурации в коде:
@ConfigSource("services.foo")
public interface FooConfig {
String bar();
Integer baz();
String propRequired();
@Nullable
String propOptional();
Integer propDefault();
String propReference();
List<String> propArray();
List<String> propArrayAsString();
Map<String, String> propMap();
@ConfigValueExtractor
public interface ObjectConfig {
String p1();
String p2();
}
ObjectConfig propObject();
List<ObjectConfig> propObjects();
}
@ConfigSource("services.foo")
interface FooConfig {
fun bar(): String
fun baz(): Int
fun propRequired(): String
fun propOptional(): String?
fun propDefault(): Int
fun propReference(): String
fun propArray(): List<String>
fun propArrayAsString(): List<String>
fun propMap(): Map<String, String>
@ConfigValueExtractor
interface ObjectConfig {
fun p1(): String
fun p2(): String
}
fun propObject(): ObjectConfig
fun propObjects(): List<ObjectConfig>
}
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Файл¶
По умолчанию ожидаются конфигурационные файлы reference.conf и application.conf.
Сначала объединяются все файлы reference.conf из classpath, затем поверх неразрешенного reference.conf накладывается
application.conf, после чего результат разрешается и проверяются обязательные подстановки.
Ожидается, что конфигурация приложения находится в application.conf, а конфигурация библиотек — в reference.conf.
HOCON также поддерживает директиву include:
файлы, подключенные через include, участвуют в том же слиянии и разрешении подстановок, что и основной файл,
и отслеживаются наблюдателем за конфигурацией, поэтому изменения во включенном файле также обновляют граф.
Приоритет выбора файла приложения для HOCON:
- Использовать файл из
config.resource, если он указан (файл из каталогаresources) - Использовать файл из
config.file, если он указан (файл из файловой системы) - Использовать
application.conf, если он присутствует (файл из каталогаresources) - Использовать пустую конфигурацию, если ничего из вышеперечисленного нет
Одновременно можно указать только одно свойство: config.resource или config.file. Если указаны оба свойства,
приложение не запустится.
YAML¶
Поддержка YAML реализована с помощью SnakeYAML.
services:
foo:
bar: "SomeValue" #(1)!
baz: 10 #(2)!
propRequired: ${REQUIRED_ENV_VALUE} #(3)!
propOptional: ${?OPTIONAL_ENV_VALUE} #(4)!
propDefault: ${?NON_DEFAULT_ENV_VALUE:10} #(5)!
propReference: ${services.foo.bar}Other${services.foo.baz} #(6)!
propArray: ["v1", "v2"] #(7)!
propArrayAsString: "v1, v2" #(8)!
propMap: #(9)!
k1: "v1"
k2: "v2"
propObject: #(10)!
p1: "v1"
p2: "v2"
propObjects: #(11)!
- p1: "v1"
p2: "v2"
- p1: "v1"
p2: "v2"
- Строковое значение конфигурации
- Числовое значение конфигурации
- Обязательное значение конфигурации, подставляемое из переменной окружения
REQUIRED_ENV_VALUE - Необязательное значение конфигурации, подставляемое из переменной окружения
OPTIONAL_ENV_VALUE; если переменная не найдена, значение конфигурации опускается - Значение конфигурации со значением по умолчанию: значение по умолчанию —
10, аNON_DEFAULT_ENV_VALUE, если найдено, заменяет его - Значение конфигурации, собранное из подстановок других частей конфигурации со значением
Otherмежду ними - Значение конфигурации в виде списка строк; значение можно задать как массив строк или как строку с разделителем-запятой
- Значение конфигурации в виде списка строк; значение можно задать как строку с разделителем-запятой или как массив строк
- Значение конфигурации в виде словаря ключ-значение
- Значение конфигурации в виде отображаемого класса
- Значение конфигурации в виде списка отображаемых классов
Представление конфигурации в коде:
@ConfigSource("services.foo")
public interface FooConfig {
String bar();
Integer baz();
String propRequired();
@Nullable
String propOptional();
Integer propDefault();
String propReference();
List<String> propArray();
List<String> propArrayAsString();
Map<String, String> propMap();
@ConfigValueExtractor
public interface ObjectConfig {
String p1();
String p2();
}
ObjectConfig propObject();
List<ObjectConfig> propObjects();
}
@ConfigSource("services.foo")
interface FooConfig {
fun bar(): String
fun baz(): Int
fun propRequired(): String
fun propOptional(): String?
fun propDefault(): Int
fun propReference(): String
fun propArray(): List<String>
fun propArrayAsString(): List<String>
fun propMap(): Map<String, String>
@ConfigValueExtractor
interface ObjectConfig {
fun p1(): String
fun p2(): String
}
fun propObject(): ObjectConfig
fun propObjects(): List<ObjectConfig>
}
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Файл¶
По умолчанию ожидаются конфигурационные файлы reference.yaml и application.yaml.
Сначала объединяются все файлы reference.yaml из classpath, затем поверх reference.yaml накладывается
application.yaml, после чего результат разрешается и проверяются обязательные подстановки.
Ожидается, что конфигурация приложения находится в application.yaml, а конфигурация библиотек — в reference.yaml.
Приоритет выбора файла приложения для YAML:
- Использовать файл из
config.resource, если он указан (файл из каталогаresources) - Использовать файл из
config.file, если он указан (файл из файловой системы) - Использовать
application.yaml, если он присутствует (файл из каталогаresources) - Использовать пустую конфигурацию, если ничего из вышеперечисленного нет
Одновременно можно указать только одно свойство: config.resource или config.file. Если указаны оба свойства,
приложение не запустится.
Пользовательская конфигурация¶
Пользовательская конфигурация отображает секцию конфигурационного файла на пользовательский тип. Затем этот тип можно внедрять как зависимость точно так же, как любой другой компонент.
Конфигурация приложения¶
Для создания пользовательских конфигураций в приложении используйте аннотацию @ConfigSource.
Она генерирует ConfigValueExtractor для интерфейса и модуль, который добавляет готовый объект конфигурации в граф
зависимостей. Значение аннотации указывает на путь секции внутри итоговой конфигурации:
Этот пример кода добавит экземпляр класса FooServiceConfig в контейнер зависимостей, который при создании будет ожидать конфигурацию следующего вида:
После этого класс FooServiceConfig уже можно использовать как зависимость в других классах:
Конфигурация библиотеки¶
Для создания пользовательских конфигураций в библиотеках используйте аннотацию @ConfigValueExtractor.
Она создает правило извлечения значения из ConfigValue<?>, но не привязывает его к конкретному пути конфигурации.
Путь выбирается в фабричном методе модуля библиотеки, поэтому одну и ту же форму конфигурации можно переиспользовать для разных секций.
@ConfigValueExtractor можно использовать на интерфейсе, record или классе Java, а также на интерфейсе или data class Kotlin.
У аннотации есть параметр mapNullAsEmptyObject (по умолчанию: true). Когда он включен, отсутствующая секция
трактуется как пустой объект: обязательные поля по-прежнему приводят к ошибке, а необязательные поля и значения по
умолчанию ведут себя так, как будто присутствовала пустая секция.
Если mapNullAsEmptyObject = false, отсутствующая секция трактуется как null для всего объекта конфигурации.
Рассмотрим такой класс конфигурации:
Чтобы библиотека предоставляла конфигурацию, реализуйте фабрику в модуле:
Фабрика будет ожидать конфигурацию следующего вида:
Затем, после подключения FooLibraryModule в приложении, FooLibraryConfig можно использовать как зависимость в других классах.
Обязательные значения¶
По умолчанию все объявленные в конфигурации значения считаются обязательными (NotNull) и должны присутствовать в
итоговой конфигурации. Если обязательное значение отсутствует или имеет значение null, приложение завершится с ошибкой
при создании объекта конфигурации.
Необязательные значения¶
Если требуется указать значение из конфигурационного файла как необязательное, можно использовать такой формат:
Предлагается использовать аннотацию @Nullable над сигнатурой метода:
@ConfigSource("services.foo")
public interface FooServiceConfig {
@Nullable//(1)!
String bar();
int baz();
}
- Подойдет любая аннотация
@Nullable, напримерjavax.annotation.Nullable/jakarta.annotation.Nullable/org.jetbrains.annotations.Nullable.
Используйте синтаксис null-safety Kotlin и пометьте параметр как nullable:
Также поддерживается тип возвращаемого значения Optional<T> (отсутствующее значение отображается на Optional.empty()),
но значение @Nullable (или nullable-тип Kotlin) является рекомендуемым стилем.
Значения по умолчанию¶
Если требуется задать значение по умолчанию при отображении конфигурации, используйте default-метод:
Гибкие имена ключей¶
Ключи конфигурации сопоставляются с гибким именованием. Имя метода сравнивается с ключом в файле не только в его точной
форме, но и в вариантах kebab-case и snake_case. Это означает, что метод someBarString() одинаково разрешается из
someBarString, some-bar-string или some_bar_string в конфигурационном файле, поэтому команды, предпочитающие ключи
в стиле kebab-case или snake_case, могут сохранять свой стиль без переименования методов.
Все три написания ключа ниже читаются в someBarString():
Рекомендуемый стиль¶
Обычно удобнее описывать конфигурацию как отдельный тип для конкретной интеграции или подсистемы: HTTP-клиента, подключения к внешнему сервису, обработчика очереди и так далее. Такой тип должен четко разделять обязательные значения, необязательные значения и значения, приходящие из переменных окружения.
В примере ниже:
baseUrl— обязательное значение из конфигурационного файлаclientName— необязательное значение из переменной окруженияORDERS_CLIENT_NAMEtoken— обязательное значение из переменной окруженияORDERS_API_TOKENrequestTimeoutимеет значение по умолчанию2sи может быть переопределено необязательной переменной окруженияORDERS_REQUEST_TIMEOUT
Это сохраняет структуру конфигурации читаемой: обязательные настройки видны в типе конфигурации, секреты можно передавать через переменные окружения, а безопасные значения по умолчанию остаются прямо в конфигурационном файле.
Внедрение конфигурации¶
Можно внедрить базовый класс ru.tinkoff.kora.config.common.Config, который представляет дерево конфигурации и дает
доступ к значениям через метод get(...). Итоговая конфигурация состоит из нескольких слоев:
- Переменные окружения
- Системные свойства
Java - Конфигурационный файл
Слои объединяются в таком порядке: переменные окружения, затем системные свойства, затем конфигурационный файл приложения. Каждый следующий слой накладывается на предыдущий.
Переменные окружения¶
Если требуется внедрить конфигурацию, содержащую только переменные окружения,
используйте аннотацию @Environment как тег для класса конфигурации:
Системные свойства¶
Если требуется внедрить конфигурацию, содержащую только системные свойства Java,
используйте аннотацию @SystemProperties как тег для класса конфигурации:
Конфигурационный файл¶
Если требуется внедрить конфигурацию приложения, состоящую только из конфигурационного файла,
используйте аннотацию @ApplicationConfig как тег для класса конфигурации:
Итоговая конфигурация¶
Если требуется внедрить полную итоговую конфигурацию приложения, которая состоит из конфигурационного файла, переменных окружения и системных свойств, просто внедрите класс конфигурации без тега:
Чтение сырого Config¶
Когда внедряется сырой Config, значения читаются через метод get(...), который возвращает узел ConfigValue<?>
для запрошенного пути. ConfigValue<?> — это sealed-тип с типизированными аксессорами: asString(), asNumber(),
asBoolean(), asObject(), asArray() и isNull(). Если значение имеет неожиданный тип, аксессор выбрасывает
ConfigValueExtractionException.
Как отмечено в разделе Рекомендации, предпочитайте типизированные пользовательские конфигурации
чтению сырого Config.
Используйте API сырого чтения только если ни как не обойтись для динамического или обобщенного доступа через ValueOf<config> чтобы не было обновления компонент.
Внимание
Мы настоятельно не рекомендуем использовать ru.tinkoff.kora.config.common.Config напрямую как зависимость в компонентах,
потому что при обновлении конфигурации будут обновлены и все компоненты графа, которые ее используют.
Мы рекомендуем всегда создавать пользовательские конфигурации.
Наблюдатель за конфигурацией¶
По умолчанию в Kora есть наблюдатель за конфигурационным файлом, который проверяет файл приложения на изменения и
запускает обновление графа зависимостей при изменении файла. Проверка выполняется каждые 1000 миллисекунд.
Для HOCON наблюдатель также отслеживает файлы, подключенные через include внутри основного конфигурационного файла.
Если такой включенный файл изменяется, конфигурация перечитывается, и граф зависимостей также обновляется.
Наблюдатель работает только для файловой конфигурации, имеющей отслеживаемый источник. Если конфигурация пришла из ресурса внутри архива или была собрана без файла приложения, на диске нечего обновлять.
Наблюдатель можно отключить с помощью:
- Переменной окружения
KORA_CONFIG_WATCHER_ENABLED(по умолчанию:true) - Системного свойства
kora.config.watcher.enabled(по умолчанию:true)
Поддерживаемые типы¶
Экстракторы конфигурации предоставляют обширный список поддерживаемых типов, который покрывает большинство значений,
которые могут понадобиться в пользовательских конфигурациях. Если стандартного преобразования недостаточно, поведение
можно расширить пользовательским компонентом ConfigValueExtractor<T>.
Список поддерживаемых типов
- boolean / Boolean
- short / Short
- int / Integer
- long / Long
- double / Double
- float / Float
- double[]
- String
- BigInteger
- BigDecimal
- Period
- Duration
- Size
- Properties
- Pattern
- UUID
- LocalDate
- LocalTime
- LocalDateTime
- OffsetTime
- OffsetDateTime
- ConfigValue.ObjectValue
- Enum (любой пользовательский
enum; отображение можно переопределить черезtoString()) Optional<T>(гдеT— любой поддерживаемый тип)List<T>(гдеT— любой поддерживаемый тип)Set<T>(гдеT— любой поддерживаемый тип)Map<String, V>илиMap<K, V>(гдеKиVподдерживаются соответствующими экстракторами)Either<A, B>(гдеAиB— любые поддерживаемые типы)
Пользовательский экстрактор¶
Если для типа нет стандартного преобразования или требуется специальная логика разбора, добавьте пользовательский
компонент ConfigValueExtractor<T>. Метод extract(...) получает значение конфигурации как ConfigValue<?>
и должен вернуть готовое значение требуемого типа.
Если конкретный экстрактор должен использоваться только для одного поля, укажите его через @Mapping:
Duration¶
Duration можно задать как число или строку.
Если указано число, оно трактуется как миллисекунды.
Если указана строка, поддерживается формат java.time.Duration, например PT10S, а также стиль HOCON:
500ms10 seconds2 minutes1h1d
Period¶
Period можно задать как число или строку.
Если указано число, оно трактуется как дни.
Если указана строка, поддерживаются такие единицы:
d/daysw/weeksm/mo/monthsy/years
Например, 7d, 2 weeks, 3mo или 1 year.
Size¶
Size — это специальный тип, который позволяет указывать размеры в байтах в удобной для человека нотации: согласно
стандарту IEEE 1541-2002 (двоичный) или стандарту
SI (десятичный).
Примеры значений:
1Mb— 1 мегабайт (1.000.000байт)1Mib— 1 мебибайт (1.048.576байт)1024b— 1024 байта1024— 1024 байта
Если указано просто число без суффикса, считается, что указаны байты.
Either¶
Either<A, B> позволяет одному полю принимать две альтернативные формы. Экстрактор сначала пробует левый тип A, и если
извлечение завершается любым исключением, откатывается к правому типу B. Это полезно, когда значение может быть либо
простым скаляром, либо структурированным объектом.
Обе эти формы допустимы для поля endpoint:
Используйте isLeft() / isRight(), чтобы проверить, какая сторона была разрешена, и left() / right(), чтобы прочитать значение.