Дано:
Сервис на 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/
Добавить комментарий