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

Skip to content

Configuration

Module is responsible for mapping the values of configuration files to classes in Kora and then using them for application settings.

HOCON

Support for HOCON is implemented with Typesafe Config. HOCON is a JSON-based config file format. The format is less strict than JSON and has a slightly different syntax.

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. String configuration value
  2. Numeric configuration value
  3. Mandatory configuration value that is substituted from the REQUIRED_ENV_VALUE environment variable.
  4. Optional configuration value which is substituted from the OPTIONAL_ENV_VALUE environment variable, if no such variable is found, the configuration value will be omitted. 5.
  5. Configuration value with default value, the default value is specified in propDefault = 10 and if NON_DEFAULT_ENV_VALUE environment variable is found, its value will replace the default value.
  6. Configuration value assembled from substitutions of other parts of the configuration and the Other value between the
  7. String list configuration value, the value is set as an array of strings or can also be set as a string with values separated by commas
  8. String list configuration value, the value is set as a string with values separated by commas or can also be set as an array of strings
  9. Configuration value as a dictionary key and value

Configuration representation in code:

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

Dependency

Dependency build.gradle:

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

Module:

@KoraApp
public interface Application extends HoconConfigModule { }

Dependency build.gradle.kts:

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

Module:

@KoraApp
interface Application : HoconConfigModule

File

By default, the configuration files reference.conf and application.conf are expected.

First, all reference.conf files are merged, second, the application.conf file is overlaid on the unresolved reference.conf file, the result is calculated and checked that all variable values are available.

It is assumed that the application configuration is in application.conf and the library configurations are in reference.conf.

Prioritize reading the application.conf configuration file:

  • Use the file from config.resource if specified (file from resources directory)
  • Use the file from config.file if specified (file from the file system)
  • Use the application.conf file if available (file from resources directory)
  • Use an empty configuration file if none of the above is present

YAML

Support for YAML is implemented using 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. String configuration value
  2. Numeric configuration value
  3. Mandatory configuration value that is substituted from the REQUIRED_ENV_VALUE environment variable.
  4. Optional configuration value which is substituted from the OPTIONAL_ENV_VALUE environment variable, if no such variable is found, the configuration value will be omitted. 5.
  5. Configuration value with default value, the default value is specified as 10 and if NON_DEFAULT_ENV_VALUE environment variable is found, its value will replace the default value.
  6. Configuration value assembled from substitutions of other parts of the configuration and the Other value between the
  7. String list configuration value, the value is set as an array of strings or can also be set as a string with values separated by commas
  8. String list configuration value, the value is set as a string with values separated by commas or can also be set as an array of strings
  9. Configuration value as a dictionary key and value

Configuration representation in code:

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

Dependency

Dependency build.gradle:

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

Module:

@KoraApp
public interface Application extends YamlConfigModule { }

Dependency build.gradle.kts:

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

Module:

@KoraApp
interface Application : YamlConfigModule

File

By default, the reference.yaml and application.yaml configuration files are expected.

First, all reference.yaml files are merged, second, the application.yaml file is overlaid on an unresolved reference.yaml file, the result is calculated and it is checked that all variable values are available.

It is assumed that the application configuration is in the application.yaml file and the library configurations are in reference.yaml.

Prioritize reading the application.yaml configuration file:

  • Use the file from config.resource if specified (file from resources directory)
  • Use the file from config.file if specified (file from the file system)
  • Use the application.yaml file if available (file from resources directory)
  • Use an empty configuration file if none of the above is present

Custom configuration

A custom configuration provides a mapping of the configuration file to a user interface. Such a user interface can later be injected as a dependency along with other components.

Application config

In order to simplify the creation of custom configurations, the @ConfigSource annotation should be used:

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

    String bar();

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

    fun bar(): String

    fun baz(): Int
}

This code sample will add an instance of the FooServiceConfig class to the dependency container, which when created will expect the following kind of configuration:

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

After that, the FooServiceConfig class can already be used as a dependency in other classes:

@Component
public final class FooService {

    private final FooServiceConfig config;

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

Library config

In order to create custom configurations within custom libraries, use the @ConfigValueExtractor annotation which will create rules for processing a configuration file into an instance of a configuration class.

Let's consider an example when there is such a configuration class:

@ConfigValueExtractor
public interface FooLibraryConfig {

    String bar();

    int baz();
}
@ConfigValueExtractor
interface FooLibraryConfig {

    fun bar(): String

    fun baz(): Int
}

In order for the library to provide configuration, you need to implement the factory in a module:

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"])!!
    }
}

The factory will expect a configuration of the following kind:

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

Then by connecting the FooLibraryModule module in the application, the FooServiceConfig config can be used as a dependency in other classes.

Required values

By default, all values declared in the config are considered required (NotNull) and must be present in the configuration file.

Optional values

If you need to specify a value from the configuration file as optional, you can use this format:

It is suggested to use the @Nullable annotation over the method signature:

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

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

    int baz();
}
  1. Any @Nullable annotation will do, such as javax.annotation.Nullable / jakarta.annotation.Nullable / org.jetbrains.annotations.Nullable / etc.

It is expected to use the Kotlin Nullability syntax and mark such a parameter as Nullable:

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

    fun bar(): String?

    fun baz(): Int
}

Default values

If there is a need to use default values in a class, you can use this format:

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

Injecting configuration

You can inject the base class ru.tinkoff.kora.config.common.Config which provides a common abstraction over the configuration file mapping. The resulting configuration mapping consists of several layers that represent:

  • Environment variables
  • System variables
  • Configuration file

Environment variables

In case you want to embed the configuration only environment variables, you can use the @Environment annotation as a tag for the configuration class:

@Component
public final class FooService {

    private final Config config;

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

System variables

In case you want to inject a configuration of only system variables, then you can use the @SystemProperties annotation as a tag for the configuration class:

@Component
public final class FooService {

    private final Config config;

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

Configuration file

In case you want to inject a complete application configuration that consists only of a configuration file, you can use the @ApplicationConfig annotation as a tag for the configuration class:

@Component
public final class FooService {

    private final Config config;

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

Resulting configuration

If you want to inject a complete application configuration that consists of a configuration file, environment variables and system variables, you simply inject the configuration class without the tag:

@Component
public final class FooService {

    private final Config config;

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

Recommendations

Tip

We do not recommend using ru.tinkoff.kora.config.common.Config directly as a dependency in components, because when you update the configuration it will cause all graph components that use it to be updated, it is recommended to always create custom user configuration interfaces.

Config Watcher

By default, Kora has a configuration file watcher that updates the contents of the configuration file, which causes the dependency graph for the affected components to be updated if the configuration file is changed.

You can disable the watcher by using:

  1. Environment variable KORA_CONFIG_WATCHER_ENABLED.
  2. System property kora.config.watcher.enabled.

Supported types

Configuration Extractors provide an extensive list of supported types that covers most of what you might need to specify in custom configurations, or you can extend the behavior with your custom ConfigValueExtractor<T> component.

List of supported types
  • 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 (any custom ENUM type)
  • List<T> (where T is any of the above listed types)
  • Set<T> (where T is any of the above types)
  • Map<String, T> (where T is any of the above types)
  • Either<A, B> (where A and B are any of the above types)

Size

Size is a special type that allows you to specify the size of bytes in a human-friendly system of calculations according to both the IEEE 1541-2002 (binary) standard and the SI (decimal) standard.

Example values:

  • 1Mb - 1 megabytes (1.000.000 bytes)
  • 1Mib - 1 megabit (1.048.576 bytes)
  • 1024b - 1024 bytes
  • 1024 - 1024 bytes

If just a number without a suffix is specified, it is considered that bytes are specified.