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

Kora is a framework for writing Java / Kotlin applications with a focus on performance, efficiency, transparency made by T-Bank / Tinkoff developers

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

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

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

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

Отображение конфигурации в коде:

@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();
}
@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>
}

Подключение

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

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:config-hocon"

Модуль:

@KoraApp
public interface Application extends HoconConfigModule { }

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

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

Модуль:

@KoraApp
interface Application : HoconConfigModule

Файл

По умолчанию ожидаются файлы конфигурации reference.conf и application.conf

Во-первых, все файлы reference.conf объединяются, во-вторых, файл application.conf накладывается на неразрешенный файл reference.conf, результат вычисляется и проверяется что все значения переменных доступны.

Предполагается что конфигурация приложения находится в файле application.conf, а конфигурации библиотек в reference.conf.

Приоритет считывания application.conf файла конфигурации:

  • Использовать файл из config.resource если указан (файл из resources директории)
  • Использовать файл из config.file если указан (файл из файловой системы)
  • Использовать файл application.conf если имеется (файл из resources директории)
  • Используется пустой файл конфигурации если все указанное выше отсутствует

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

Отображение конфигурации в коде:

@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();
}
@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>
}

Подключение

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

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:config-yaml"

Модуль:

@KoraApp
public interface Application extends YamlConfigModule { }

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

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

Модуль:

@KoraApp
interface Application : YamlConfigModule

Файл

По умолчанию ожидаются файлы конфигурации reference.yaml и application.yaml.

Во-первых, все файлы reference.yaml объединяются, во-вторых, файл application.yaml накладывается на неразрешенный файл reference.yaml, результат вычисляется и проверяется что все значения переменных доступны.

Предполагается что конфигурация приложения находится в файле application.yaml, а конфигурации библиотек в reference.yaml.

Приоритет считывания application.yaml файла конфигурации:

  • Использовать файл из config.resource если указан (файл из resources директории)
  • Использовать файл из config.file если указан (файл из файловой системы)
  • Использовать файл application.yaml если имеется (файл из resources директории)
  • Используется пустой файл конфигурации если все указанное выше отсутствует

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

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

В приложении

Для создания пользовательских конфигураций следует использовать аннотацию @ConfigSource:

@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 которая создаст правила обработки файла конфигурации в экземпляр класса конфигурации.

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

@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 в приложении, конфиг FooServiceConfig можно использовать как зависимость в других классах.

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

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

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

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

Предлагается использовать аннотацию @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 / и т.д.

Предполагается использовать Kotlin Nullability синтаксис и помечать такой параметр как Nullable:

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

    fun bar(): String?

    fun baz(): Int
}

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

Если есть необходимость использовать задать в отображении значение по умолчанию, то можно воспользоваться 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
    }
}

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

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

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

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

В случае если требуется внедрить конфигурацию только переменных окружения, то для этого можно использовать аннотацию @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)

Системные переменные

В случае если требуется внедрить конфигурацию только системных переменных, то для этого можно использовать аннотацию @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)

Рекомендации

Совет

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

Наблюдатель

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

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

  1. Переменной окружения KORA_CONFIG_WATCHER_ENABLED
  2. Системного свойства kora.config.watcher.enabled

Поддерживаемые типы

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

Список поддерживаемых типов
  • boolean / Boolean
  • short / Short
  • int / Integer
  • long / Long
  • double / Double
  • float / Float
  • double[]
  • String
  • BigInteger
  • BigDecimal
  • Period
  • Duration
  • Size
  • Properties
  • Pattern
  • UUID
  • Properties
  • LocalDate
  • LocalTime
  • LocalDateTime
  • OffsetTime
  • OffsetDateTime
  • Enum (любой пользовательский ENUM тип)
  • List<T> (где T любой из выше перечисленных типов)
  • Set<T> (где T любой из выше перечисленных типов)
  • Map<String, T> (где T любой из выше перечисленных типов)
  • Either<A, B> (где A и B любой из выше перечисленных типов)