Как стать автором
Обновить

Spring Boot @ConfigurationProperties и коллекции

Время на прочтение5 мин
Количество просмотров11K

Дано:

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

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

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии2

Публикации

Истории

Работа

Java разработчик
395 вакансий

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн