Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Перейти к содержанию

Конфигурация

Модуль конфигурации читает настройки приложения из файлов 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"
        }
      ]
    }
}
  1. Строковое значение конфигурации
  2. Числовое значение конфигурации
  3. Обязательное значение конфигурации, подставляемое из переменной окружения REQUIRED_ENV_VALUE
  4. Необязательное значение конфигурации, подставляемое из переменной окружения OPTIONAL_ENV_VALUE; если переменная не найдена, значение конфигурации опускается
  5. Значение конфигурации со значением по умолчанию: значение по умолчанию задается как propDefault = 10, а NON_DEFAULT_ENV_VALUE, если найдено, заменяет его
  6. Значение конфигурации, собранное из подстановок других частей конфигурации со значением Other между ними
  7. Значение конфигурации в виде списка строк; значение можно задать как массив строк или как строку с разделителем-запятой
  8. Значение конфигурации в виде списка строк; значение можно задать как строку с разделителем-запятой или как массив строк
  9. Значение конфигурации в виде словаря ключ-значение
  10. Значение конфигурации в виде отображаемого класса
  11. Значение конфигурации в виде списка отображаемых классов

Значения также могут ссылаться на другие ключи конфигурации (само-ссылка / перекрестная ссылка) через ${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:

implementation "ru.tinkoff.kora:config-hocon"

Модуль:

@KoraApp
public interface Application extends HoconConfigModule { }

Зависимость build.gradle.kts:

implementation("ru.tinkoff.kora:config-hocon")

Модуль:

@KoraApp
interface Application : HoconConfigModule

Файл

По умолчанию ожидаются конфигурационные файлы 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. Если указаны оба свойства, приложение не запустится.

Пример указания конфигурации при запуске через java:

java -Dconfig.file=path/to/configFile application

Пример указания конфигурации в build.gradle:

run {
    jvmArgs += [
            "-Dconfig.file=path/to/configFile"
    ]
}

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"
  1. Строковое значение конфигурации
  2. Числовое значение конфигурации
  3. Обязательное значение конфигурации, подставляемое из переменной окружения REQUIRED_ENV_VALUE
  4. Необязательное значение конфигурации, подставляемое из переменной окружения OPTIONAL_ENV_VALUE; если переменная не найдена, значение конфигурации опускается
  5. Значение конфигурации со значением по умолчанию: значение по умолчанию — 10, а NON_DEFAULT_ENV_VALUE, если найдено, заменяет его
  6. Значение конфигурации, собранное из подстановок других частей конфигурации со значением Other между ними
  7. Значение конфигурации в виде списка строк; значение можно задать как массив строк или как строку с разделителем-запятой
  8. Значение конфигурации в виде списка строк; значение можно задать как строку с разделителем-запятой или как массив строк
  9. Значение конфигурации в виде словаря ключ-значение
  10. Значение конфигурации в виде отображаемого класса
  11. Значение конфигурации в виде списка отображаемых классов

Представление конфигурации в коде:

@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:

implementation "ru.tinkoff.kora:config-yaml"

Модуль:

@KoraApp
public interface Application extends YamlConfigModule { }

Зависимость build.gradle.kts:

implementation("ru.tinkoff.kora:config-yaml")

Модуль:

@KoraApp
interface Application : YamlConfigModule

Файл

По умолчанию ожидаются конфигурационные файлы 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. Если указаны оба свойства, приложение не запустится.

Пример указания конфигурации при запуске через java:

java -Dconfig.file=path/to/configFile application

Пример указания конфигурации в build.gradle:

run {
    jvmArgs += [
            "-Dconfig.file=path/to/configFile"
    ]
}

Пользовательская конфигурация

Пользовательская конфигурация отображает секцию конфигурационного файла на пользовательский тип. Затем этот тип можно внедрять как зависимость точно так же, как любой другой компонент.

Конфигурация приложения

Для создания пользовательских конфигураций в приложении используйте аннотацию @ConfigSource. Она генерирует ConfigValueExtractor для интерфейса и модуль, который добавляет готовый объект конфигурации в граф зависимостей. Значение аннотации указывает на путь секции внутри итоговой конфигурации:

@ConfigSource("services.foo")
public interface FooServiceConfig {

    String bar();

    int baz();
}
@ConfigSource("services.foo")
interface FooServiceConfig {

    fun bar(): String

    fun baz(): Int
}

Этот пример кода добавит экземпляр класса FooServiceConfig в контейнер зависимостей, который при создании будет ожидать конфигурацию следующего вида:

services {
  foo {
    bar = "SomeValue"
    baz = 10
  }
}
services:
  foo:
    bar: "SomeValue"
    baz: 10

После этого класс FooServiceConfig уже можно использовать как зависимость в других классах:

@Component
public final class FooService {

    private final FooServiceConfig config;

    public FooService(FooServiceConfig config) {
        this.config = config;
    }
}
@Component
class FooService(val config: FooServiceConfig)

Конфигурация библиотеки

Для создания пользовательских конфигураций в библиотеках используйте аннотацию @ConfigValueExtractor. Она создает правило извлечения значения из ConfigValue<?>, но не привязывает его к конкретному пути конфигурации. Путь выбирается в фабричном методе модуля библиотеки, поэтому одну и ту же форму конфигурации можно переиспользовать для разных секций. @ConfigValueExtractor можно использовать на интерфейсе, record или классе Java, а также на интерфейсе или data class Kotlin.

У аннотации есть параметр mapNullAsEmptyObject (по умолчанию: true). Когда он включен, отсутствующая секция трактуется как пустой объект: обязательные поля по-прежнему приводят к ошибке, а необязательные поля и значения по умолчанию ведут себя так, как будто присутствовала пустая секция. Если mapNullAsEmptyObject = false, отсутствующая секция трактуется как null для всего объекта конфигурации.

Рассмотрим такой класс конфигурации:

@ConfigValueExtractor
public interface FooLibraryConfig {

    String bar();

    int baz();
}
@ConfigValueExtractor
interface FooLibraryConfig {

    fun bar(): String

    fun baz(): Int
}

Чтобы библиотека предоставляла конфигурацию, реализуйте фабрику в модуле:

public interface FooLibraryModule {

    default FooLibraryConfig config(Config config, ConfigValueExtractor<FooLibraryConfig> extractor) {
        return extractor.extract(config.get("library.foo"));
    }
}
interface FooLibraryModule {

    fun config(config: Config, extractor: ConfigValueExtractor<FooLibraryConfig>): FooLibraryConfig {
        return extractor.extract(config["library.foo"])!!
    }
}

Фабрика будет ожидать конфигурацию следующего вида:

library {
  foo {
    bar = "SomeValue"
    baz = 10
  }
}
library:
  foo:
    bar: "SomeValue"
    baz: 10

Затем, после подключения FooLibraryModule в приложении, FooLibraryConfig можно использовать как зависимость в других классах.

Обязательные значения

По умолчанию все объявленные в конфигурации значения считаются обязательными (NotNull) и должны присутствовать в итоговой конфигурации. Если обязательное значение отсутствует или имеет значение null, приложение завершится с ошибкой при создании объекта конфигурации.

Необязательные значения

Если требуется указать значение из конфигурационного файла как необязательное, можно использовать такой формат:

Предлагается использовать аннотацию @Nullable над сигнатурой метода:

@ConfigSource("services.foo")
public interface FooServiceConfig {

    @Nullable//(1)!
    String bar();

    int baz();
}
  1. Подойдет любая аннотация @Nullable, например javax.annotation.Nullable / jakarta.annotation.Nullable / org.jetbrains.annotations.Nullable.

Используйте синтаксис null-safety Kotlin и пометьте параметр как nullable:

@ConfigSource("services.foo")
interface FooServiceConfig {

    fun bar(): String?

    fun baz(): Int
}

Также поддерживается тип возвращаемого значения Optional<T> (отсутствующее значение отображается на Optional.empty()), но значение @Nullable (или nullable-тип Kotlin) является рекомендуемым стилем.

Значения по умолчанию

Если требуется задать значение по умолчанию при отображении конфигурации, используйте default-метод:

@ConfigSource("services.foo")
public interface FooServiceConfig {

    String bar();

    default int baz() {
        return 42;
    }
}
@ConfigSource("services.foo")
interface FooServiceConfig {

    fun bar(): String

    fun baz(): Int {
        return 42
    }
}

Гибкие имена ключей

Ключи конфигурации сопоставляются с гибким именованием. Имя метода сравнивается с ключом в файле не только в его точной форме, но и в вариантах kebab-case и snake_case. Это означает, что метод someBarString() одинаково разрешается из someBarString, some-bar-string или some_bar_string в конфигурационном файле, поэтому команды, предпочитающие ключи в стиле kebab-case или snake_case, могут сохранять свой стиль без переименования методов.

@ConfigValueExtractor
public interface BarConfig {

    String someBarString();
}
@ConfigValueExtractor
interface BarConfig {

    fun someBarString(): String
}

Все три написания ключа ниже читаются в someBarString():

bar {
  someBarString = "value"        //(1)!
  # some-bar-string = "value"    //(2)!
  # some_bar_string = "value"    //(3)!
}
  1. Точное написание имени метода в camelCase
  2. Гибкое написание в kebab-case
  3. Гибкое написание в snake_case
bar:
  someBarString: "value"         #(1)!
  # some-bar-string: "value"     #(2)!
  # some_bar_string: "value"     #(3)!
  1. Точное написание имени метода в camelCase
  2. Гибкое написание в kebab-case
  3. Гибкое написание в snake_case

Обычно удобнее описывать конфигурацию как отдельный тип для конкретной интеграции или подсистемы: HTTP-клиента, подключения к внешнему сервису, обработчика очереди и так далее. Такой тип должен четко разделять обязательные значения, необязательные значения и значения, приходящие из переменных окружения.

В примере ниже:

  1. baseUrl — обязательное значение из конфигурационного файла
  2. clientName — необязательное значение из переменной окружения ORDERS_CLIENT_NAME
  3. token — обязательное значение из переменной окружения ORDERS_API_TOKEN
  4. requestTimeout имеет значение по умолчанию 2s и может быть переопределено необязательной переменной окружения ORDERS_REQUEST_TIMEOUT
import java.time.Duration;
import javax.annotation.Nullable;

@ConfigSource("clients.orders")
public interface OrdersClientConfig {

    String baseUrl();

    @Nullable
    String clientName();

    String token();

    Duration requestTimeout();
}
import java.time.Duration

@ConfigSource("clients.orders")
interface OrdersClientConfig {

    fun baseUrl(): String

    fun clientName(): String?

    fun token(): String

    fun requestTimeout(): Duration
}
clients {
  orders {
    baseUrl = "https://orders.example.com"
    clientName = ${?ORDERS_CLIENT_NAME}
    token = ${ORDERS_API_TOKEN}
    requestTimeout = 2s
    requestTimeout = ${?ORDERS_REQUEST_TIMEOUT}
  }
}
clients:
  orders:
    baseUrl: "https://orders.example.com"
    clientName: ${?ORDERS_CLIENT_NAME}
    token: ${ORDERS_API_TOKEN}
    requestTimeout: ${?ORDERS_REQUEST_TIMEOUT:2s}

Это сохраняет структуру конфигурации читаемой: обязательные настройки видны в типе конфигурации, секреты можно передавать через переменные окружения, а безопасные значения по умолчанию остаются прямо в конфигурационном файле.

Внедрение конфигурации

Можно внедрить базовый класс ru.tinkoff.kora.config.common.Config, который представляет дерево конфигурации и дает доступ к значениям через метод get(...). Итоговая конфигурация состоит из нескольких слоев:

  • Переменные окружения
  • Системные свойства Java
  • Конфигурационный файл

Слои объединяются в таком порядке: переменные окружения, затем системные свойства, затем конфигурационный файл приложения. Каждый следующий слой накладывается на предыдущий.

Переменные окружения

Если требуется внедрить конфигурацию, содержащую только переменные окружения, используйте аннотацию @Environment как тег для класса конфигурации:

@Component
public final class FooService {

    private final Config config;

    public FooService(@Environment Config config) {
        this.config = config;
    }
}
@Component
class FooService(@Environment val config: Config)

Системные свойства

Если требуется внедрить конфигурацию, содержащую только системные свойства Java, используйте аннотацию @SystemProperties как тег для класса конфигурации:

@Component
public final class FooService {

    private final Config config;

    public FooService(@SystemProperties Config config) {
        this.config = config;
    }
}
@Component
class FooService(@SystemProperties val config: Config)

Конфигурационный файл

Если требуется внедрить конфигурацию приложения, состоящую только из конфигурационного файла, используйте аннотацию @ApplicationConfig как тег для класса конфигурации:

@Component
public final class FooService {

    private final Config config;

    public FooService(@ApplicationConfig Config config) {
        this.config = config;
    }
}
@Component
class FooService(@ApplicationConfig val config: Config)

Итоговая конфигурация

Если требуется внедрить полную итоговую конфигурацию приложения, которая состоит из конфигурационного файла, переменных окружения и системных свойств, просто внедрите класс конфигурации без тега:

@Component
public final class FooService {

    private final Config config;

    public FooService(Config config) {
        this.config = config;
    }
}
@Component
class FooService(val config: Config)

Чтение сырого Config

Когда внедряется сырой Config, значения читаются через метод get(...), который возвращает узел ConfigValue<?> для запрошенного пути. ConfigValue<?> — это sealed-тип с типизированными аксессорами: asString(), asNumber(), asBoolean(), asObject(), asArray() и isNull(). Если значение имеет неожиданный тип, аксессор выбрасывает ConfigValueExtractionException.

@Component
public final class FooService {

    public FooService(Config config) {
        ConfigValue<?> value = config.get("services.foo.bar");
        if (!value.isNull()) {
            String bar = value.asString();
        }
    }
}
@Component
class FooService(config: Config) {

    init {
        val value = config["services.foo.bar"]
        if (!value.isNull) {
            val bar = value.asString()
        }
    }
}

Как отмечено в разделе Рекомендации, предпочитайте типизированные пользовательские конфигурации чтению сырого Config. Используйте API сырого чтения только если ни как не обойтись для динамического или обобщенного доступа через ValueOf<config> чтобы не было обновления компонент.

Внимание

Мы настоятельно не рекомендуем использовать ru.tinkoff.kora.config.common.Config напрямую как зависимость в компонентах, потому что при обновлении конфигурации будут обновлены и все компоненты графа, которые ее используют. Мы рекомендуем всегда создавать пользовательские конфигурации.

Наблюдатель за конфигурацией

По умолчанию в Kora есть наблюдатель за конфигурационным файлом, который проверяет файл приложения на изменения и запускает обновление графа зависимостей при изменении файла. Проверка выполняется каждые 1000 миллисекунд.

Для HOCON наблюдатель также отслеживает файлы, подключенные через include внутри основного конфигурационного файла. Если такой включенный файл изменяется, конфигурация перечитывается, и граф зависимостей также обновляется.

Наблюдатель работает только для файловой конфигурации, имеющей отслеживаемый источник. Если конфигурация пришла из ресурса внутри архива или была собрана без файла приложения, на диске нечего обновлять.

Наблюдатель можно отключить с помощью:

  1. Переменной окружения KORA_CONFIG_WATCHER_ENABLED (по умолчанию: true)
  2. Системного свойства 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<?> и должен вернуть готовое значение требуемого типа.

public final class TokenConfigValueExtractor implements ConfigValueExtractor<Token> {

    @Override
    public Token extract(ConfigValue<?> value) {
        if (value instanceof ConfigValue.NullValue) {
            return null;
        }
        return new Token(value.asString());
    }
}
class TokenConfigValueExtractor : ConfigValueExtractor<Token> {

    override fun extract(value: ConfigValue<*>): Token? {
        if (value is ConfigValue.NullValue) {
            return null
        }
        return Token(value.asString())
    }
}

Если конкретный экстрактор должен использоваться только для одного поля, укажите его через @Mapping:

@ConfigValueExtractor
public interface ApiConfig {

    @Mapping(TokenConfigValueExtractor.class)
    Token token();
}
@ConfigValueExtractor
interface ApiConfig {

    @Mapping(TokenConfigValueExtractor::class)
    fun token(): Token
}

Duration

Duration можно задать как число или строку. Если указано число, оно трактуется как миллисекунды. Если указана строка, поддерживается формат java.time.Duration, например PT10S, а также стиль HOCON:

  • 500ms
  • 10 seconds
  • 2 minutes
  • 1h
  • 1d

Period

Period можно задать как число или строку. Если указано число, оно трактуется как дни. Если указана строка, поддерживаются такие единицы:

  • d / days
  • w / weeks
  • m / mo / months
  • y / 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. Это полезно, когда значение может быть либо простым скаляром, либо структурированным объектом.

import ru.tinkoff.kora.common.util.Either;

@ConfigValueExtractor
public interface EndpointConfig {

    String host();

    int port();
}

@ConfigSource("services.foo")
public interface FooServiceConfig {

    Either<String, EndpointConfig> endpoint();
}
import ru.tinkoff.kora.common.util.Either

@ConfigValueExtractor
interface EndpointConfig {

    fun host(): String

    fun port(): Int
}

@ConfigSource("services.foo")
interface FooServiceConfig {

    fun endpoint(): Either<String, EndpointConfig>
}

Обе эти формы допустимы для поля endpoint:

services {
  foo {
    endpoint = "https://example.com"   //(1)!
  }
}
  1. Разрешается как левый тип (String)
services:
  foo:
    endpoint:                          #(1)!
      host: "example.com"
      port: 8080
  1. Разрешается как правый тип (EndpointConfig)

Используйте isLeft() / isRight(), чтобы проверить, какая сторона была разрешена, и left() / right(), чтобы прочитать значение.