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<ReportProperties> ) {      data class ReportProperties(         val filePrefix: String,         val regionName: String,         val sftpHost: String,         val sftpPort: Int,         val sftpUser: String     ) }

Оказалось, что это не лучшее решение использовать список, если вы планируете размещать часть свойств его элементов в разных 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<NestedListProperties> = listOf() ) {      data class NestedProperties(         val nestedField1: String = "",         val nestedField2: String = ""     )      data class NestedListProperties(         val nestedListField1: String = "",         val nestedListField2: String = ""     ) }
@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 вполне себе справляется с разрешением этих неоднозначностей. А как думаете Вы?

Это мой первый пробный пост. Надеюсь, он не получился слишком поверхностным.


ссылка на оригинал статьи https://habr.com/ru/post/686726/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *