Дано:
Сервис на 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 вполне себе справляется с разрешением этих неоднозначностей. А как думаете Вы?
Это мой первый пробный пост. Надеюсь, он не получился слишком поверхностным.