1. Схема конфигурации как договорённость
Если вы когда-нибудь открывали чужой проект и видели там пять конфигурационных файлов с одинаковыми ключами в трёх местах, вы знаете ощущение: «оно работает, но почему — не спрашивайте». Схема конфигурации нужна именно чтобы убрать гадание. Мы хотим, чтобы любой участник команды мог ответить на вопрос: «где лежит настройка X и почему берётся именно это значение».
Самое важное здесь — понять, что Spring Boot даёт нам механики (import, профили, external locations), но он не может вместо нас принять архитектурное решение «какой файл за что отвечает». То есть Boot — это не «сделай мне красиво», а «вот инструменты, собери себе понятную систему». И мы сейчас эту систему соберём так, чтобы она была устойчивой, расширяемой и при этом не превращалась в отдельную дисциплину «конфиг-инженеринг».
Чтобы было проще держать это в голове, можно думать о конфигурации как о чемоданах в поездке. Один огромный чемодан, в котором и носки, и ноутбук, и шампунь, и документы, — вроде бы «всё в одном месте», но найти паспорт в момент посадки в поезд становится приключением. Схема конфигурации — это когда вы честно раскладываете вещи: документы в отдельную папку, одежду отдельно, аптечку отдельно. Да, файлов больше, но жизнь спокойнее.
2. Структура файлов catalog-service
Когда мы говорим «схема», полезно сразу зафиксировать физическую структуру. Мы не изобретаем космический корабль: нам нужен небольшой Boot-сервис без БД, но конфигурация у него всё равно должна быть модульной. Наш целевой набор файлов на этом этапе выглядит так:
src/main/resources/
├── application.yaml
├── application-local.yaml
├── application-dev.yaml
├── application-prod.yaml
└── catalog-data.yaml
./config/
└── catalog-extra.yaml (опционально, локальный override)
Здесь есть три смысловых уровня: базовый конфиг приложения, профильные отличия и отдельный файл данных каталога. А ещё есть внешний «кармашек» для локальных/частных переопределений, который не хочется коммитить или запихивать в профильные файлы. Важно: внешний файл — это именно слой override, а не «вторая копия конфигурации».
Давайте прямо словами проговорим ответственность, чтобы потом не зависеть от памяти.
| Артефакт | Где находится | Для чего нужен | Какой стиль изменений ожидается |
|---|---|---|---|
| application.yaml | внутри jar (resources) | базовая рамка приложения: имя, общие флаги, import-декларации, дефолтные значения | редактируется редко, осознанно, почти всегда коммитится |
| application-{profile}.yaml | внутри jar | различия окружений: «как вести себя локально/в dev/в prod» | меняется по мере роста проекта, но не должен дублировать весь конфиг |
| catalog-data.yaml | внутри jar | большой блок данных каталога: список курсов и связанные данные | меняется, когда меняются данные домена, но не должен смешиваться с окруженческими настройками |
| ./config/catalog-extra.yaml | снаружи (filesystem) | опциональный локальный слой переопределения, чтобы не трогать ресурсы и не коммитить личные настройки | может отсутствовать вообще; если есть — держим его небольшим |
Обратите внимание на тонкую вещь: catalog-data.yaml — это не «ещё один application-файл». Он существует потому, что данные каталога (список курсов) по объёму и смыслу другие, чем настройки самого приложения. Условно говоря, «как работает сервис» и «какие курсы в каталоге» — две разные темы, и им полезно жить отдельно.
3. Слои и приоритеты конфигурации
Когда конфигурация становится модульной, главный страх новичка звучит так: «А как понять, кто победит, если одно и то же свойство задано в нескольких местах?» Правильный ответ: в идеале — не задавать одно и то же свойство в нескольких местах без причины. Precedence — это не инструмент повседневной работы, а страховочная сетка. В здоровой схеме большинство ключей имеет «единственного владельца».
Поэтому для catalog-service полезнее держать в голове не универсальную лестницу всех возможных sources в мире, а каноническую раскладку ролей. Она отвечает не на вопрос «кто выше абсолютно всех», а на вопрос «какой файл за что отвечает в нашем проекте».
| Слой / файл | Роль в catalog-service | Что там живёт |
|---|---|---|
| application.yaml | базовая рамка приложения | spring.application.name, общие defaults, декларации spring.config.import |
| catalog-data.yaml | отдельный data-блок, подключённый через spring.config.import | список курсов и связанные данные каталога |
| application-{profile}.yaml | профильные отличия | только то, что реально зависит от local/dev/prod |
| ./config/catalog-extra.yaml | внешний optional override по явному file:-import | личные локальные переопределения без правки ресурсов и без коммита |
Такую схему легко проговорить вслух: база живёт в application.yaml, большие данные вынесены в отдельный импортируемый файл, профильные документы меняют только окруженческие различия, а внешний catalog-extra.yaml — это небольшой локальный слой удобства. Как только один и тот же ключ начинает бесконтрольно кочевать между этими файлами, схема превращается в лотерею.
Поверх этой файловой раскладки, как и раньше, могут прийти env vars, system properties и CLI args. Они по-прежнему умеют переопределять значения, но это уже отдельные каналы runtime-управления. В каноническую схему файлов catalog-service мы их не включаем, чтобы не смешивать раскладку документов с внешним управлением запуском.
Каталоги через spring.config.additional-location и wildcard — это уже вариант расширения этой схемы, а не её обязательная часть. Они полезны, когда нужен внешний пакет стандартных application*.yaml, но базовый snapshot проекта на этом не строится.
4. Базовый application.yaml и import
Базовый application.yaml в здоровом проекте — как оглавление в книге. Он не должен содержать всю книгу целиком, его задача — задать рамку: как называется приложение, какие у него базовые defaults, какие дополнительные конфигурационные части надо подключить. Если вы держите это в голове, рука перестаёт тянуться «дописать ещё чуть-чуть в base», и файлы начинают жить своей ролью, а не историей случайных правок.
В нашем catalog-service базовый файл должен быть коротким, и его легко будет пролистывать глазами. Мы оставим в нём spring.application.name, наши пользовательские свойства app.catalog.* (только те, что действительно общие), и spring.config.import для двух вещей: catalog-data.yaml из classpath и внешнего optional override файла.
Пример базового application.yaml:
# src/main/resources/application.yaml
spring:
application:
# Имя сервиса (влияет на логи, метрики, многие автоконфиги)
name: catalog-service
config:
# Подключаем дополнительные источники конфигурации.
# Важно: порядок в import влияет на то, что сможет переопределить что.
# 1) Данные каталога (внутри jar)
# 2) Локальный внешний override (может отсутствовать)
import: "classpath:catalog-data.yaml,optional:file:./config/catalog-extra.yaml"
app:
catalog:
# Общие дефолты (не зависят от окружения)
title: "Spring+ Catalog"
max-featured-count: 4
default-published-only: true
# Удобный флаг для старта: печатаем мини-отчёт (не про секреты)
startup-report-enabled: true
Обратите внимание на две важные идеи.
Первая: catalog-data.yaml не подхватится сам, потому что это не application*.yaml. Он «обычный файл», и чтобы он стал частью конфигурации, мы подключаем его через spring.config.import. Да, он лежит в resources, но Boot не читает каждый YAML в resources из любопытства.
Вторая: внешний файл мы подключаем как optional:file:.... Это означает, что он может существовать на вашей машине, а может не существовать у коллеги или на CI — и от этого мир не должен рушиться. Мы специально выбираем «мягкий старт» для этого слоя, потому что это локальная удобная надстройка, а не обязательная часть контракта приложения.
Есть ещё один аккуратный момент: импортируемый файл (если в нём есть такие же ключи) может переопределить значения из application.yaml. Поэтому самый здоровый стиль — не дублировать ключи между base и импортируемым файлом. И мы так и сделаем: base держит рамку, catalog-data.yaml держит данные, а не базовые флаги.
5. Профили и внешние переопределения
Профили очень легко начать использовать как «копию всей конфигурации для каждой среды». Это выглядит логично первые два дня, а на третий день вы внезапно правите одну и ту же настройку в трёх файлах и забываете про четвёртый. Поэтому мы держимся принципа: профильные файлы меняют только то, что реально зависит от среды. Всё остальное остаётся в base или в импортируемых data-файлах.
Начнём с самого безопасного: менять app.catalog.title, чтобы визуально сразу видеть, что вы в local, dev или prod. Это банально, но это отличный «маркер среды» без тяжёлой инфраструктуры.
Пример application-local.yaml:
# src/main/resources/application-local.yaml
app:
catalog:
# Маркер окружения: чтобы в UI/логах не перепутать local и dev/prod
title: "Spring+ Catalog (local)"
Пример application-dev.yaml:
# src/main/resources/application-dev.yaml
app:
catalog:
# Маркер окружения: dev
title: "Spring+ Catalog (dev)"
Пример application-prod.yaml:
# src/main/resources/application-prod.yaml
app:
catalog:
# В prod обычно не хотим "весёлых" суффиксов
title: "Spring+ Catalog"
Заметьте, что мы не копируем сюда max-featured-count, default-published-only и тем более список курсов. Профили — это не «место, где удобно продублировать всё», это «место, где можно аккуратно поменять только нужное».
Теперь про внешний override-файл ./config/catalog-extra.yaml. Его роль — точечные изменения без правки resources. Типичный сценарий: вы хотите локально поднять лимит featured, или временно выключить стартовый отчёт, или подложить «свой» набор курсов для демо — и не коммитить это.
Пример внешнего файла:
# ./config/catalog-extra.yaml
app:
catalog:
# Локальный override: можно временно подкрутить параметры, не трогая ресурсы в jar
max-featured-count: 6
startup-report-enabled: false
Этот файл подключается через import, поэтому он не обязан называться application.yaml. Это важная разница с «директорией конфигурации», которую Boot сканирует на стандартные имена. То есть catalog-extra.yaml — произвольное имя, и оно не станет “магически видимым”, если вы просто положите его рядом. Мы сделали его видимым тем, что указали в spring.config.import конкретную file:-локацию.
На этом каноническая схема catalog-service уже собрана: application.yaml как рамка, catalog-data.yaml как data-блок, profile-файлы как environment deltas и ./config/catalog-extra.yaml как небольшой локальный override. Обычно этого достаточно. Не стоит без причины смешивать сюда ещё и directory-search-механику: explicit import произвольного файла и поиск стандартных application*.yaml решают разные задачи.
Если нужен не один точечный файл, а целый внешний каталог со стандартными application*.yaml, тогда в ход идёт отдельный variant через spring.config.additional-location. Это уже не базовый snapshot проекта, а расширение для запусков, где конфиги приезжают папкой.
Например, запуск с отдельной директорией стандартных конфигов может выглядеть так:
# Внешний каталог со стандартными application*.yaml
./gradlew bootRun --args="--spring.profiles.active=local --spring.config.additional-location=optional:file:./custom-config/"
Здесь Boot ищет именно application.yaml, application-local.yaml и другие стандартные имена в ./custom-config/. Это не замена нашему ./config/catalog-extra.yaml, а другой способ организовать внешний слой.
Если такие конфиги разложены по нескольким подкаталогам первого уровня, можно использовать wildcard:
# Несколько внешних подкаталогов со стандартными application*.yaml
./gradlew bootRun --args="--spring.config.additional-location=optional:file:./custom-config/*/"
Это удобно, но порядок слоёв уже зависит от имён каталогов. Поэтому wildcard лучше считать удобным расширением, а не каноном проекта. Если порядок критичен, явный список директорий обычно безопаснее, чем надежда на алфавит.
Диагностика итоговой конфигурации
Когда конфигурация становится многослойной, мозг начинает пытаться «симулировать» поведение Boot в голове, и это обычно заканчивается фразой «ну, наверное, оно так работает». В этот момент полезно добавить маленький диагностический якорь: кусочек кода, который на старте печатает итоговые значения пары ключей. Не чтобы заменить понимание логов и Actuator (это будет позже), а чтобы в моменте не гадать, какой слой победил.
У нас уже есть подходящее место: StartupSummaryRunner (или любой ApplicationRunner), который живёт в catalog.bootstrap. Пока мы не дошли до нормального логирования, можно использовать System.out.println как временный «фонарик». Главное — не превращать это в вечную религию println и не печатать секреты.
Пример очень компактного runner’а:
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
class StartupSummaryRunner implements ApplicationRunner {
private final Environment env;
StartupSummaryRunner(Environment env) {
// Environment уже содержит итоговую «склейку» всех слоёв конфигурации
this.env = env;
}
@Override
public void run(ApplicationArguments args) {
// Читаем итоговые значения: если ключ не найден — используем дефолт
String title = env.getProperty("app.catalog.title", "Catalog");
// Полезно увидеть активные профили, чтобы не гадать «почему подхватилось не то»
String profiles = String.join(",", env.getActiveProfiles());
// Важно: не печатайте секреты (пароли, токены) через println даже «временно»
System.out.println("Active profiles = " + profiles); // Active profiles = local
System.out.println("app.catalog.title = " + title); // app.catalog.title = Spring+ Catalog (local)
}
}
Что это нам даёт практически? Если вы запускаете приложение с --spring.profiles.active=local и параллельно меняете профильный файл или внешний override, вы сразу видите результат «после сборки всех слоёв». И если вдруг title не тот, который вы ожидали, вы не бежите сразу в StackOverflow, а начинаете проверять схему: какой слой должен владеть этим ключом, не задали ли вы его в двух местах, не перебили ли внешним файлом.
Кстати, это же место отлично иллюстрирует, почему мы так упорно настаиваем на «одном владельце ключа». Если вы держите app.catalog.title одновременно в application.yaml, в application-local.yaml и ещё в catalog-extra.yaml, то runner будет печатать итог, но объяснить, почему он именно такой, будет трудно. А если title меняется только профилем — объяснение занимает одну фразу, и вы не превращаетесь в археолога по YAML.
6. Типичные ошибки при сборке конфигурационной схемы
Ошибка №1: один и тот же ключ живёт в трёх файлах «на всякий случай».
Это самая распространённая причина конфигурационных мистерий. Технически Spring Boot честно выберет победителя по правилам приоритетов, но вы сами перестанете понимать систему. Если ключ должен зависеть от среды, пусть он живёт в profile-файлах. Если он общий — пусть живёт в base. Если он локально-личный — пусть живёт во внешнем override. Схема должна быть объяснимой, а не «работающей по счастливой случайности».
Ошибка №2: тащить список курсов в application-local.yaml, потому что «мне так удобнее для локалки».
Профильные файлы — не место для больших data-блоков. Как только вы положите туда список курсов, у вас появится второй «источник правды», а потом — и третий. В итоге вы будете тестировать на одном наборе данных, а другой разработчик — на другом, и вы начнёте спорить не о коде, а о реальности. Большие данные каталога должны жить в catalog-data.yaml, а профили должны менять только окруженческое поведение.
Ошибка №3: ожидать, что catalog-extra.yaml подхватится просто потому, что он лежит в ./config/.
Каталог, подключённый как config location, ищет стандартные application*.yaml. Произвольные имена вроде catalog-extra.yaml он не «угадает». Если вы хотите использовать произвольное имя, его надо подключить либо через spring.config.import (как мы сделали), либо через точную spring.config.location/additional-location с указанием конкретного файла. Надеяться на «авось Boot догадается» — плохая стратегия: Boot догадается ровно о том, что вы ему явно сказали.
Ошибка №4: положить spring.config.location внутрь того application.yaml, который вы пытаетесь найти через location.
Это как написать на двери «ключ от этой двери лежит внутри комнаты». spring.config.location и spring.config.additional-location читаются очень рано, и их обычно задают извне. Если вам нужно управлять местами поиска, делайте это аргументами запуска, env vars или system properties, иначе вы получите неуправляемую конструкцию, где «настройка настройки» пытается загрузиться после того, как настройка уже понадобилась.
Ошибка №5: делать обязательный внешний файл optional: (или наоборот) без осознанного решения.
optional: — это не украшение. Если без внешнего файла приложение не соответствует ожидаемой модели (например, это обязательный слой, без которого нельзя стартовать), тогда отсутствие файла должно валить запуск, и optional: там вреден. Если это локальная надстройка «хочу — подложу», тогда optional: делает запуск удобнее и честнее. Смешивать эти два подхода — значит получать «успешный старт» с неправильным поведением, и это гораздо хуже, чем честное падение на старте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ