- Kotlin Spring Boot Configuration properties best practices
- Naming
- Anti-patterns
- Difference between configuration and properties
- Configuration properties bean must be initialised by Spring not manually
- Standards, code consistency
- Demo
- Summary
- Spring Boot @ConfigurationProperties и коллекции
- Kotlin Spring @ConfigurationProperties
- 2. Kotlin Spring @ConfigurationProperties
- 3. Nested property with Kotlin @ConfigurationProperties
- 4. Register Spring @ConfigurationProperties in a standard way
- 5. Register using the @EnableConfigurationProperties
- 6. Conclusion
Kotlin Spring Boot Configuration properties best practices
Basically method level beans are the same as class level but you can create N properties beans from a single class.
The properties itself can be propagated in different ways but the most common is through application properties / yaml files.
What’s important about the properties that they can be:
Naming
Spring follows the next conventions as for the configuration (class with beans) and configuration properties classes:
It’s good to follow the standards from the ecosystem you use to prevent confusions as:
Properties class is not a configuration
Anti-patterns
Difference between configuration and properties
- Configuration — is a class that defines beans
- Properties — is a class that defines data structure to describe the application properties, e.g. to map the properties to an object
The next example is anti-pattern because spring works differently with these concepts:
@Configuration @ConfigurationProperties class MyProperties String value; >
Configuration properties bean must be initialised by Spring not manually
The next example is anti-pattern because configuration properties is a bean can be initialised much more naturally when it’s loosely coupled with it’s configuration, for instance it’s a problem if you use @ConstructorBinding :
@Component @ConfigurationProperties class MyProperties String value; >
Standards, code consistency
The goal of the best practices is to find the golden mean. Where we expect that the data-structure represents in a neat way the mapping between the properties and the class.
- default value should be located as close as possible to the field so developer can immediately see it
- nullability should represent the actual necessity of the property
- it should be simple
- composite children fields are designed in the same way
Having all the requirements we have the next example:
Demo
Let’s say we have this set of properties defined:
io.github.artemptushkin.example: defaultOverriddenProperty: overridden requiredStringProperty: required-value notNullListProperty: ["foo", "baz"]
it will give us the next runtime properties:
io.github.artemptushkin.example.defaultedStringProperty=default io.github.artemptushkin.example.defaultOverriddenProperty=overridden io.github.artemptushkin.example.requiredStringProperty=required-value io.github.artemptushkin.example.optionalStringProperty=null io.github.artemptushkin.example.notNullListProperty[0]=foo io.github.artemptushkin.example.notNullListProperty[1]=baz
- @ConstructorBinding let’s us use create immutable objects
- Kotlin default constructor values let us use to have default values right in place in one single line
- Always initialise collections as empty by default to prevent nullable field
Summary
It’s possible to write clean classes and keep them concise from one project to another and within your company.
I hope this noted helped you.
Spring Boot @ConfigurationProperties и коллекции
Сервис на spring boot(2.6.4) + kotlin(1.5.31) по выгрузке произвольного количества отчётов по крону. Каждый отчет имеет свои настройки. Для конфигурирования списка свойств отчётов используется собственно список. Для инжекта в приложение используется data class с аннотацией @ConfigurationProperties, где одно из свойств — список data class.
Выглядит это примерно так:
#application.yml report-export: cron: 0 0 12 * * reports: - file-prefix: REPORT1 region-name: Москва sftp-host: host1 sftp-port: 22 sftp-user: user1 sftp-password: pwd1 - file-prefix: REPORT2 region-name: Подольск sftp-host: host1 sftp-port: 22 sftp-user: user1 sftp-password: pwd2
@ConfigurationProperties("report-export") @ConstructorBinding data class ReportExportProperties( val cron: String, val reports: List )
Оказалось, что это не лучшее решение использовать список, если вы планируете размещать часть свойств его элементов в разных property source. Конкретно в моем случае, секрет sftp-password должен был быть размещен в Vault.
Согласно документации списки для @ConfigurationProperties мёрджутся не так, как для остальных сложенных структур. Если положить в Vault только секреты sftp-password, то остальные свойства data class из списка будут инициализироваться дефолтными свойствами, если они заданы ( если не заданы — контекст не поднимется). Т.е. список не мёрджится, а берется из property source с большим приоритетом как есть.
Ниже тест, показывающий, показывающий, что для вложенных классов и свойств, мёрдж работает, а для списков — нет:
#application.yml tpp: test: root: root-field1: default nested: nested-field1: default nested-list: - nested-list-field1: default1 - nested-list-field1: default2 #application-dev.yml tpp: test: root: root-field2: dev nested: nested-field2: dev nested-list: - nested-list-field2: dev1 - nested-list-field2: dev2
@ConfigurationProperties("tpp.test.root") @ConstructorBinding data class RootPropperties( val rootField1: String = "", val rootField2: String = "", val nested: NestedProperties = NestedProperties(), val nestedList: List = listOf() )
@ActiveProfiles("dev") @SpringBootTest internal class ConfigurationPropertiesTest < @Autowired lateinit var rootPropperties: RootPropperties @Test fun `configuration properties binding`() < assertEquals("default", rootPropperties.rootField1) assertEquals("dev", rootPropperties.rootField2) assertEquals("dev", rootPropperties.nested.nestedField2) assertEquals("default", rootPropperties.nested.nestedField1) assertTrue(rootPropperties.nestedList.isNotEmpty()) assertEquals("dev1", rootPropperties.nestedList[0].nestedListField2) assertEquals("dev2", rootPropperties.nestedList[1].nestedListField2) // Здесь падает // org.opentest4j.AssertionFailedError: //Expected :default1 //Actual : assertEquals("default1", rootPropperties.nestedList[0].nestedListField1) // Здесь падает // org.opentest4j.AssertionFailedError: //Expected :default2 //Actual : assertEquals("default2", rootPropperties.nestedList[1].nestedListField1) >>
Интересно, что если переписать тест, запрашивая все значения из Environment, то мы получим корректные значения для всех вложенных структур:
@ActiveProfiles("dev") @SpringBootTest internal class ConfigurationPropertiesTest < @Autowired lateinit var environment: Environment @Test fun `environment binding`() < assertEquals("default", environment.getProperty("tpp.test.root.root-field1")) assertEquals("dev", environment.getProperty("tpp.test.root.root-field2")) assertEquals("default", environment.getProperty("tpp.test.root.nested.nested-field1")) assertEquals("dev", environment.getProperty("tpp.test.root.nested.nested-field2")) assertEquals("default1", environment.getProperty("tpp.test.root.nested-list[0].nested-list-field1")) assertEquals("dev1", environment.getProperty("tpp.test.root.nested-list[0].nested-list-field2")) assertEquals("default2", environment.getProperty("tpp.test.root.nested-list[1].nested-list-field1")) assertEquals("dev2", environment.getProperty("tpp.test.root.nested-list[1].nested-list-field2")) >>
В ответе на вопрос в моем issue сказано, что так сделано намеренно и каких-либо изменений для списков не планируется. Предложенный workaround — использовать Map вместо коллекций. Ниже приведен пример, как можно переписать предыдущий тест:
#application.yml tpp: test: root-map: root-field1: default nested: nested-field1: default nested-map: 1: nested-map-field1: default1 2: nested-map-field1: default2 #application-dev.yml tpp: test: root-map: root-field2: dev nested: nested-field2: dev nested-map: 1: nested-map-field2: dev1 2: nested-map-field2: dev2
@ActiveProfiles("dev") @SpringBootTest internal class ConfigurationPropertiesMapTest < @Autowired lateinit var environment: Environment @Autowired lateinit var rootPropperties: RootMapPropperties @Test fun `configuration properties binding`() < Assertions.assertEquals("default", rootPropperties.rootField1) Assertions.assertEquals("dev", rootPropperties.rootField2) Assertions.assertEquals("default", rootPropperties.nested.nestedField1) Assertions.assertEquals("dev", rootPropperties.nested.nestedField2) Assertions.assertTrue(rootPropperties.nestedMap.isNotEmpty()) Assertions.assertEquals("default1", rootPropperties.nestedMap["1"]. nestedMapField1) Assertions.assertEquals("dev1", rootPropperties.nestedMap["1"]. nestedMapField2) Assertions.assertEquals("default2", rootPropperties.nestedMap["2"]. nestedMapField1) Assertions.assertEquals("dev2", rootPropperties.nestedMap["2"]. nestedMapField2) >>
У меня нет однозначного мнения относительно ответа от команды spring, что не планируется пересматривать алгоритм мёрджа для коллекций. С одной стороны понятно, что есть некая неоднозначность относительно того, что делать, если например в одном property source 2 элемента, в другом 3. Как это интерпретировать, мёрджить ли этот список сложением элементов например? Мое мнение, что можно мёрджить элементы списка также, как Map относительно индексов. Т.е. первые 2 элемента мёрджутся согласно приоритетам property source, а 3й полностью берется как есть, т.к. он задан только в одном property source. Как показано выше Environment вполне себе справляется с разрешением этих неоднозначностей. А как думаете Вы?
Это мой первый пробный пост. Надеюсь, он не получился слишком поверхностным.
Kotlin Spring @ConfigurationProperties
In this article, we will learn the Kotlin Spring @ConfigurationProperties.
2. Kotlin Spring @ConfigurationProperties
You may have used the @Value annotation to inject the configuration properties in your spring application. However, if you are working with multiple properties or properties which are hierarchical in nature, it could be cumbersome and introduce boilerplate code.
So Spring Boot provides an alternative method @ConfigurationProperties of working with properties that let strongly typed beans govern and validate the configuration of your application.
You can add this @ConfigurationProperties annotation to a class definition or a @Bean method in a @Configuration class if you want to bind and validate some external Properties (e.g. from a .properties file).
3. Nested property with Kotlin @ConfigurationProperties
Consider that the application property file contains the following properties.
server.error.path=/error server.port=9000
You can bind the above properties to Kotlin fields by annotating the class using the @ConfigurationProperties . Note that the @ConfigurationProperties support properties that are hierarchical.
For example, the server.error.path is hierarchical and nested property i.e., the root server prefix property contains the error nested property. The @ConfigurationProperties support multiple prefixes that have the same root prefix like below:
@Configuration @ConfigurationProperties(value = "server") class AppProperties < var address: String? = null var error: Error = Error() inner class Error < var path: String? = null >>
@SpringBootApplication open class ConfigurationPropertiesApplication : CommandLineRunner < @Autowired lateinit var appProperties : AppProperties private val logger: Logger = LoggerFactory.getLogger(ConfigurationPropertiesApplication::class.java) override fun run(vararg args: String?) < logger.info(appProperties.address) logger.info(appProperties.error.path) >> fun main(args: Array) < runApplication(*args) >
However, there are scenarios where the properties have completely different prefixes.
For example, the following property file contains different prefixes serverA and serverB .
servera.address=127.0.0.1 servera.port=9000 serverb.error=custom serverb.port=8080
Let’s create a Kotlin POJO class that maps the properties to fields.
You can create different beans for each prefix by using the same ServerProperties file in your application configuration file.
@Configuration class AppConfig < @Bean @ConfigurationProperties(prefix = "servera") fun serverA(): ServerProperties < return ServerProperties() >@Bean @ConfigurationProperties(prefix = "serverb") fun serverB(): ServerProperties < return ServerProperties() >>
You can use the AppConfig bean to access both the property prefixes in your spring boot application.
@SpringBootApplication @EnableConfigurationProperties(AppProperties::class) open class ConfigurationPropertiesApplication : CommandLineRunner < /*@Autowired lateinit var appProperties : AppProperties*/ @Autowired lateinit var appConfig : AppConfig private val logger: Logger = LoggerFactory.getLogger(ConfigurationPropertiesApplication::class.java) override fun run(vararg args: String?) < logger.info(appConfig.serverA().address) logger.info(appConfig.serverB().error) >> fun main(args: Array) < runApplication(*args) >
4. Register Spring @ConfigurationProperties in a standard way
You can register the @ConfigurationProperties beans in the standard way (for example using @Bean methods, using @Component or @Configuration annotations). You can refer to this article to understand bean instantiation.
For example, the following AppProperties class is annotated with @Component , so the Spring container detects this bean during component scanning and then instantiates, and registers it as a bean.
@Component @ConfigurationProperties(value = "server") class AppProperties < var address: String? = null var error: Error = Error() inner class Error < var path: String? = null >>
You can access the fields or properties of this bean later in your application. For example, we have auto wired the appProperties bean and accessing it just like any other Spring bean.
@SpringBootApplication open class ConfigurationPropertiesApplication : CommandLineRunner < @Autowired lateinit var appProperties : AppProperties private val logger: Logger = LoggerFactory.getLogger(ConfigurationPropertiesApplication::class.java) override fun run(vararg args: String?) < logger.info(appProperties.address) logger.info(appProperties.error.path) >> fun main(args: Array) < runApplication(*args) >
However, if you remove the annotation @Component , then the following error appears on the application start-up.
Field appProperties in com.tedblob.profiles.ProfilesApplication required a bean of type 'com.tedblob.profiles.configurations.AppProperties' that could not be found. The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) Action: Consider defining a bean of type 'com.tedblob.profiles.configurations.AppProperties' in your configuration.
5. Register using the @EnableConfigurationProperties
Alternatively, you can also use this @EnableConfigurationProperties annotation to support the @ConfigurationProperties for convenience.
For example, the @EnableConfigurationProperties takes the list of @ConfigurationProperty classes as input and allows the Spring container to detect and manage them as regular Spring beans.
@SpringBootApplication @EnableConfigurationProperties(AppProperties::class) open class ConfigurationPropertiesApplication : CommandLineRunner < @Autowired lateinit var appProperties : AppProperties private val logger: Logger = LoggerFactory.getLogger(ConfigurationPropertiesApplication::class.java) override fun run(vararg args: String?) < logger.info(appProperties.address) logger.info(appProperties.error.path) >>
6. Conclusion
To sum up, we have learned the Kotlin Spring @ConfigurationProperties.