Spring Boot предоставляет широкие возможности для внешней конфигурации приложения. Благодаря этому один и тот же артефакт приложения можно запускать в разных средах, передавая значения из различных источников, таких как:

  • файлы свойств;

  • переменные окружения;

  • системные свойства;

  • аргументы командной строки.

В этой статье мы рассмотрим лучшие практики управления конфигурацией приложений Spring Boot.

Хорошо продуманная стратегия конфигурации должна обеспечивать следующее:

  • конфигурация остается отделенной от кода приложения;

  • приложение не запускается, если обязательная конфигурация отсутствует или некорректна;

  • значения по умолчанию можно переопределять для каждой среды развертывания;

  • конфиденциальные значения предоставляются специализированной системой управления секретами.

Классификация параметров конфигурации

Как правило, конфигурацию приложения Spring Boot можно разделить на три категории:

  • Значения приложения по умолчанию: безопасные, несекретные значения, например тайм-ауты к сторонним сервисам и ограничения на количество повторных попыток. Храните их вместе с приложением.

  • Конфигурация развертывания: значения, определяющие конкретную среду, например адреса серверов баз данных, имена очередей и URL внешних сервисов. Передавайте их через платформу развертывания.

  • Секреты: пароли, API-ключи, сертификаты и закрытые ключи. Храните их в специализированной системе управления секретами.

Например, application.properties может содержать значения конфигурации приложения по умолчанию:

app.promotion-service.base-url=http://localhost:8181
app.promotion-service.timeout=3s
app.promotion-service.retries=3
logging.level.com.jetbrains=DEBUG
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false

Значение по умолчанию должно быть безопасным для любой среды, в которой оно может использоваться. Такие параметры, как URL базы данных и учетные данные, никогда не следует жестко прописывать в коде приложения. Если для обязательного параметра нет безопасного значения по умолчанию, проверяйте его наличие при запуске.

Используйте @ConfigurationProperties для привязки свойств приложения

Приложения Spring могут получать доступ к значениям конфигурации через Environment, @Value или @ConfigurationProperties.

Используйте Environment, когда имена свойств необходимо определять динамически или инфраструктурному коду нужен прямой доступ к источникам свойств.

Используйте @Value для отдельных значений:

PromotionService(
  @Value("${app.promotion-service.base-url}") String baseUrl,
  @Value("${app.promotion-service.timeout}") Duration timeout,
  @Value("${app.promotion-service.retries}") int retries) {
    this.baseUrl = baseUrl;
    this.timeout = timeout;
    this.retries = retries;
}

Разрозненные выражения @Value затрудняют поиск имен свойств, их валидацию и рефакторинг. Отдельный тип конфигурации на основе @ConfigurationProperties поддерживает все эти возможности.

Для связанных параметров конфигурации предпочтительнее использовать @ConfigurationProperties. Эта аннотация обеспечивает:

  • типобезопасную привязку и преобразование значений;

  • гибкую привязку между именами свойств и членами Java-классов;

  • валидацию на уровне группы;

  • автодополнение и навигацию в IDE через сгенерированные метаданные.

Например, при интеграции со сторонним REST API нам может понадобиться настроить базовый URL сервиса, тайм-аут и количество повторных попыток.

app.promotion-service.base-url=${PROMOTION_SERVICE_URL}
app.promotion-service.timeout=${PROMOTION_SERVICE_TIMEOUT:3s}
app.promotion-service.retries=3

В этой конфигурации значение base-url задается из переменной окружения PROMOTION_SERVICE_URL, а значение timeout из переменной окружения PROMOTION_SERVICE_TIMEOUT со значением по умолчанию 3 секунды.

Spring Boot поддерживает привязку через сеттеры. Свойства можно привязать к классу, использующему сеттеры, следующим образом:

@ConfigurationProperties(prefix = "app.promotion-service")
public class PromotionSvcProperties {

    private String baseUrl;
    private Duration timeout;
    private int retries;

    // Setters and getters
}

Зарегистрируйте типы конфигурации с помощью @ConfigurationPropertiesScan:

@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Аннотация @ConfigurationPropertiesScan ищет компоненты, помеченные @ConfigurationProperties, и регистрирует их как Spring-бины.

Теперь PromotionSvcProperties можно внедрять в другие Spring-бины и получать через него значения свойств.

Для привязки @ConfigurationProperties отдавайте предпочтение record

Как правило, конфигурация задается во время запуска и остается неизменной на протяжении всего жизненного цикла приложения.

Для большинства конфигураций приложения предпочтительным вариантом является Java record. Он изначально обеспечивает неизменяемость, поэтому значения нельзя случайно изменить, в отличие от привязки через обычный класс, где можно непреднамеренно вызвать сеттер:

@ConfigurationProperties(prefix = "app.promotion-service")
public record PromotionSvcProperties(
        String baseUrl,
        Duration timeout,
        int retries) {
}

Механизм гибкой привязки Spring Boot сопоставляет канонические имена в kebab-case, например base-url, с полем baseUrl.

Иногда требуется привязать свойства к бину, предоставляемому сторонней библиотекой, исходный код которой нельзя изменить, чтобы добавить аннотацию @ConfigurationProperties.

Чтобы напрямую привязать параметры конфигурации к стороннему классу, объявите его как @Bean и пометьте метод бина аннотацией @ConfigurationProperties:

@Configuration
public class ClientConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "third-party.client")
    public ThirdPartyClientProperties clientProperties() {
        return new ThirdPartyClientProperties();
    }
}

Свойства third-party.client можно настроить следующим образом:

third-party.client.base-url=https://api.example.com
third-party.client.connect-timeout=5s
third-party.client.read-timeout=30s

Если сторонний класс неизменяемый или не поддерживает привязку через сеттеры, создайте собственный класс свойств и используйте его для создания стороннего объекта:

@ConfigurationProperties(prefix = "third-party.client")
public record ClientProperties(
    URI baseUrl,
    Duration connectTimeout,
    Duration readTimeout
) {}


@Configuration
@EnableConfigurationProperties(ClientProperties.class)
class ClientConfiguration {

    @Bean
    ThirdPartyClient thirdPartyClient(ClientProperties properties) {
        return new ThirdPartyClient(
                properties.baseUrl(),
                properties.connectTimeout(),
                properties.readTimeout()
        );
    }
}

Подход с оберткой, как правило, предпочтительнее, поскольку он позволяет избежать прямой зависимости конфигурации приложения от структуры классов сторонней библиотеки.

Ошибки нужно выявлять как можно раньше: проверяйте конфигурацию при запуске

Ошибки конфигурации следует выявлять во время запуска приложения и немедленно останавливать запуск, если обязательная конфигурация отсутствует или содержит некорректные значения. Добавьте @Validated к бину @ConfigurationProperties и примените к его свойствам ограничения Jakarta Bean Validation.

@Validated
@ConfigurationProperties(prefix = "app.promotion-service")
public record PromotionSvcProperties(
 @NotBlank String baseUrl,
        @NotNull Duration timeout,
        @Min(1) @Max(5) int retries,
        @NotNull @Valid SyncProperties sync) {

    public record SyncProperties(@NotEmpty String cron) {
    }
}

Если spring-boot-starter-validation присутствует в classpath, ошибки привязки или валидации остановят запуск приложения. Проверяйте обязательные значения, числовые диапазоны, вложенные группы и другие ограничения на уровне приложения.

Используйте классы-обертки, если необходимо отличать отсутствие значения от значения Java по умолчанию. Например, Integer с аннотацией @NotNull позволяет определить, что значение отсутствует, тогда как int по умолчанию получает значение 0.

Значения по умолчанию также можно задавать с помощью @DefaultValue:

@Validated
@ConfigurationProperties(prefix = "app.promotion-service")
public record PromotionSvcProperties(
 @NotBlank String baseUrl,
        @NotNull Duration timeout,
        @Min(1) @Max(5) @DefaultValue("3") Integer retries,
        @NotNull @Valid SyncProperties sync) {

    public record SyncProperties(@DefaultValue("0 0 * * * *") String cron) {
    }
}

В этом примере с помощью @DefaultValue заданы значения по умолчанию для свойств retries и cron. Они будут использоваться, если соответствующие свойства не настроены.

Учитывайте приоритет источников свойств

Spring Boot объединяет несколько источников свойств. Если одно и то же свойство задано более чем в одном источнике, итоговое значение берется из источника с более высоким приоритетом.

Ниже приведен упрощенный порядок наиболее часто используемых при развертывании приложения источников, от самого низкого к самому высокому приоритету:

application.properties/yaml (низкий приоритет)
           ↓
файлы конфигурации для конкретных профилей
           ↓
переменные окружения ОС
           ↓
системные свойства Java
           ↓
аргументы командной строки    (высокий приоритет)

Порядок загрузки конфигурации в Spring Boot важно учитывать при поиске причины, по которой фактическое значение отличается от ожидаемого.

Переменные окружения широко поддерживаются операционными системами, контейнерными средами выполнения и облачными платформами. Spring Boot формирует имена переменных окружения из канонических имен свойств: заменяет точки символами подчеркивания, удаляет дефисы и преобразует результат в верхний регистр:

app.payment-timeout     -> APP_PAYMENT_TIMEOUT
spring.datasource.url   -> SPRING_DATASOURCE_URL
Комментарий от Михаила Поливаха

Данная практика называется Relaxed Binding.

https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.relaxed-binding

Строго говоря, Spring Boot нормализует имена property в Environment не на переменные окружения, а на определенный внутренний формат. Такая нормализация происходит не только для переменных окружения, но и для свойств, пришедших из других PropertySource-ов, например из System Property и т.д.

Определить итоговое значение свойства бывает непросто, если оно задано сразу в нескольких источниках конфигурации. IntelliJ IDEA может отображать фактические значения конфигурации в виде встроенных подсказок прямо в редакторе. При выборе такой подсказки IDE показывает источник, из которого получено значение, а также указывает, было ли оно переопределено другим источником, например переменной окружения или системным свойством.

IDE также обеспечивает навигацию между объявлениями свойств, членами @ConfigurationProperties и местами использования свойств. Для пользовательских конфигурационных свойств эта поддержка дополнительно расширяется за счет метаданных, генерируемых spring-boot-configuration-processor.

Храните секреты в специализированной системе

Не храните пароли, API-ключи, сертификаты или закрытые ключи в системе контроля версий. Используйте такие решения, как HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager, Azure Key Vault или аналогичный сервис вашей платформы.

Убедитесь, что секреты не попадают в логи, сообщения об ошибках, метаданные конфигурации и общедоступные management endpoints.

ПРИМЕЧАНИЕ: В непроизводственных средах endpoint env модуля Actuator может помочь определить источник фактического значения свойства. Его не следует делать общедоступным, поскольку конфигурация может содержать конфиденциальные данные.

Комментарий от Михаила Поливаха

В теории, сокрыть sensative значения на деле в Spring Boot можно, но там прямо целая большая история с этим.

Было время, когда Spring Boot пытался понять самостоятельно, что прятать, а что нет. Было время, когда это можно было как-то по ключам настроить. Потом подход стал больше полагаться на SantizationFunction и т.д.

В общем случае, на данный момент, для современных Spring Boot приложений (Boot 3 и выше), если вы используете ендпоинты Actuator в том или ином виде, то вам придется маскировать либо все, либо никакие property в рамках Environment. 

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide/b677938c7f72eafe86ae3620b8662d796509d654#actuator-endpoints-sanitization

Это можно в теории настроить самим, написав собственную функцию санитизации. Тем не менее, на практике это довольно не просто, т.к. на практике это включает в себя в т.ч. какой-либо RBAC в лучшем случае, а в худшем ещё и ABAC.

Axelix, кстати из коробки дает возможность просто с помощью property сказать, какие свойства не должны быть видны никому, кроме администраторов в системе: 

https://axelix.io/docs/ru/setting-up-spring-boot-service/spring-boot-starter/configuration#%D0%BC%D0%B0%D1%81%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D1%87%D1%83%D0%B2%D1%81%D1%82%D0%B2%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D1%85-%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%

Рекомендуемые подходы к управлению конфигурацией

Не существует единого подхода к управлению конфигурацией, который подходил бы для любого приложения. Выбирайте стратегию с учетом архитектуры приложения, среды развертывания и уровня сложности.

Монолит

Для монолитного приложения храните общие значения по умолчанию в самом приложении, используйте файлы для отдельных профилей только там, где это действительно необходимо, а переопределения, зависящие от среды развертывания, передавайте через переменные окружения. Конфиденциальные значения храните в специализированном менеджере секретов.

Контейнерные приложения

Для приложений, работающих на контейнерной платформе, например Kubernetes, храните разумные значения по умолчанию в приложении, а конфигурацию, зависящую от конкретного развертывания, передавайте через ConfigMaps. Секреты храните отдельно в специализированной системе управления секретами.

Микросервисы

Для микросервисной архитектуры стоит рассмотреть Spring Cloud Config Server, чтобы централизовать управление конфигурацией, правилами ее использования и версионированием. Секреты при этом по-прежнему следует хранить в специализированной системе управления секретами.

Комментарий от Михаила Поливаха

В целом, Spring Cloud Config Server можно использовать для "хранения" секретов в том числе. Хранение тут в кавычках, т.е. Spring Cloud Config Server по сути выступает лишь некоторым прокси сервером для конфигуарции Spring Boot приложений

У Axelix, кстати, благодаря контрибьютеру из участников Spring АйО сообщества, появилась поддержка Spring Cloud Config Server, которая будет срелижена уже совсем скоро, в 1.1.0 minor релизе :)

https://axelix.io/docs/ru/setting-up-master-ui/configuring-master/#%D0%B7%D0%B0%D0%B3%D1%80%D1%83%D0%B7%D0%BA%D0%B0-%D0%BA%D0%BE%D0%BD%D1%84%D0%B8%D0%B3%D1%83%D1%80%D0%B0%D1%86%D0%B8%D0%B8-%D0%B8%D0%B7-spring-cloud-config-server

Итоги

Эффективное управление конфигурацией приложения начинается с разумных значений по умолчанию, типобезопасных @ConfigurationProperties и проверки конфигурации при запуске. Значения, зависящие от среды, следует хранить вне приложения, учитывать приоритет источников свойств, а секреты в свою очередь помещать в специализированную систему управления секретами.

Подходящая стратегия конфигурации должна соответствовать архитектуре приложения и среде его развертывания.

Во время локальной разработки и удаленной отладки IntelliJ IDEA помогает увидеть фактическую конфигурацию: показывает итоговые значения свойств и их источники, подсвечивает переопределения и обеспечивает навигацию между конфигурационными файлами и связанными с ними Java-свойствами.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.