1. Правило одного формата
Профили — штука полезная, но коварная: они дают ощущение «о, теперь я могу всё контролировать», и именно поэтому новички часто превращают конфигурацию в набор копий одного и того же файла с минимальными отличиями, где через неделю уже страшно что-то менять. Эта лекция — про дисциплину. Мы не добавляем новые механики Boot, мы наводим порядок в уже известных: один формат, понятные роли файлов, минимум дублей и быстрая проверка итогового состояния.
Когда конфигурация аккуратная, у вас появляется предсказуемость: вы легко отвечаете на вопросы «почему порт другой», «почему в dev видно больше данных», «почему prod запускается иначе». Когда конфигурация хаотичная, начинается гадание на кофейной гуще: «кажется, оно где-то переопределяется… но где?». И да, гадать на гуще можно, но обычно это делают в отпуске, а не в продакшене.
Если вы когда-нибудь видели проект, где одновременно лежат application.yaml, application-dev.yaml, application.properties, application-prod.properties, а где-то ещё спрятан config.properties, то вы знаете это чувство: «я боюсь трогать конфиг, потому что не понимаю, что победит». В Spring Boot действительно можно смешивать форматы, но в учебном проекте (и в большинстве реальных небольших сервисов) это почти всегда ухудшает ситуацию. Наша цель — не показать «все варианты», а зафиксировать нормальный baseline: YAML-first.
Когда рядом лежат .yaml и .properties
Spring Boot умеет читать и .properties, и .yaml. Если оба файла присутствуют в одном и том же месте поиска (например, в src/main/resources), то они оба будут загружены, но порядок наложения сделает поведение менее очевидным для новичка. На практике это выглядит так: вы правите YAML, ожидаете эффект, а эффекта нет — потому что рядом лежит .properties, который переопределил тот же ключ. Очень «весёлый» баг, особенно когда вы уверены, что правите «главный конфиг».
Мини-антипример:
# src/main/resources/application.yaml
# База в YAML: задаём значение через привычную YAML-структуру
app:
catalog:
# Этот ключ легко переопределить в другом источнике (и не заметить)
title: "Catalog From YAML"
# src/main/resources/application.properties
# Properties лежит рядом и может «победить» по приоритету
app.catalog.title=Catalog From Properties
Если вы потом в коде сделаете:
import org.springframework.core.env.Environment;
// Предполагаем, что Environment уже получен из Spring-контекста
String title = environment.getProperty("app.catalog.title");
System.out.println(title); // Catalog From Properties (победил .properties)
то получите значение из .properties. И дальше начинается типичная студенческая история: «Spring Boot игнорирует YAML» (нет), «у меня кеши сломались» (тоже не всегда), «IDE не подхватила» (виновата IDE, конечно). На самом деле всё проще: два файла на один ключ — два источника правды.
Вывод для курса и проекта
В catalog-service мы принимаем простое правило: внутри приложения один основной формат конфигурации — YAML. Это не запрет «по закону», это инженерная гигиена, которая резко уменьшает количество неожиданных конфликтов. Если в проекте в будущем появятся какие-то .properties (например, из-за чужой интеграции или старого шаблона), это должно восприниматься как исключение, которое вы либо убираете, либо документируете, либо осознанно переносите в YAML.
Чтобы правило было не «про красивые слова», а про поведение репозитория, полезно хотя бы раз сделать такой рефакторинг: удалить application.properties, если он появился автоматически, и оставить только YAML-файлы. Да, иногда это решение выглядит как «убрать лишний файл», но в конфигурации «лишний файл» — это как «лишний провод» в электрощитке: пока не искрит, кажется нормально, а потом внезапно не до шуток.
2. Роли конфигурационных файлов
Когда появляются профили, важно не только «создать application-local.yaml», но и договориться, какую роль играет каждый файл. Если роли не определены, файлы начнут расти как сорняки: в одном профиле окажется половина общих значений, в другом — другая половина, а базовый файл превратится в непонятный «остаток». Наша цель — сделать так, чтобы конфигурация читалась как документ: общая часть, затем небольшие поправки для окружения.
Простая карта ответственности
Ниже — очень практическая таблица. Её можно буквально распечатать и повесить на стену рядом с монитором, чтобы профили не превращались в «копипаст-параллельную вселенную».
| Файл | Что хранит | Как читать |
|---|---|---|
| application.yaml | Общие значения и структура app.catalog.*, которые не зависят от окружения | «Это то, как приложение устроено в целом» |
| application-local.yaml | Только локальные отличия для запуска на машине разработчика | «Удобства для меня, но не для всей команды» |
| application-dev.yaml | Общий dev-baseline (общие “не продовые” настройки) | «Командная среда разработки» |
| application-prod.yaml | Только отличия продового режима | «Без локальных послаблений» |
Если вы начинаете сомневаться «куда положить ключ», попробуйте простой вопрос: это значение должно быть одинаковым во всех средах? Тогда оно в application.yaml. Это значение связано именно с моим ноутбуком (порт, более “широкие” дефолты для просмотра данных)? Тогда в application-local.yaml. Это значение — общее для dev-окружения команды? Тогда application-dev.yaml. Это значение — требование для более строгого поведения? Тогда application-prod.yaml.
Наглядная схема наложения слоёв
Вот простая схема, которая помогает «почувствовать» поведение Boot без мистики:
flowchart TD
A["application.yaml (base)"] --> B["application-local.yaml (override)"]
A --> C["application-dev.yaml (override)"]
A --> D["application-prod.yaml (override)"]
B --> E["Итоговая конфигурация (если активен local)"]
C --> F["Итоговая конфигурация (если активен dev)"]
D --> G["Итоговая конфигурация (если активен prod)"]
Важно: profile-файл не «заменяет» базовый конфиг. Он добавляется поверх него и перекрывает только совпавшие ключи. Поэтому базовый файл должен быть максимально полноценным по структуре, а profile-файлы — короткими, как хороший комментарий в коде: «вот тут отличие».
3. Минимум дублей: “пишем дельту”
Самая частая ошибка, которую вы увидите в реальных проектах (и часто в своих первых проектах — да, мы все через это проходили), выглядит так: берём application.yaml, копируем в application-dev.yaml, меняем два ключа и живём. Через месяц меняем базовый файл, забываем обновить копии, и получаем «почему в dev одно, а в prod другое — хотя должно быть одинаково». Поэтому в этой части мы фиксируем стиль: profile-файл содержит только то, что реально отличается.
Плохой стиль: profile-файл как «вторая версия всего приложения»
Смотрите, как выглядит плохой application-dev.yaml: он почти полностью повторяет базу.
# Плохой стиль: почти полная копия базы (создаёт второй источник правды)
spring:
application:
name: "catalog-service"
app:
catalog:
title: "Course Catalog (dev)"
max-featured-count: 4
default-published-only: false
startup-report-enabled: true
maintenance-mode: false
Проблема здесь не в том, что «так нельзя». Проблема в том, что это создаёт два источника правды. Теперь, если вы захотите поменять max-featured-count с 4 на 6 как общую настройку, вам нужно помнить, что вы должны поменять и базу, и dev, и local, и prod (если там тоже копия). А если забудете, то приложение будет вести себя «по-разному» без видимой причины, и вы потратите время на расследование, хотя сами и создали повод.
Хороший стиль: profile-файл как короткая заметка “только отличия”
Вот «хорошая» версия того же application-dev.yaml, где остаются только отличия.
# Хороший стиль: короткий файл, только отличия относительно application.yaml
app:
catalog:
# В dev меняем только то, что реально хотим отличать от базы
title: "Course Catalog (dev)"
default-published-only: false
Этот файл маленький, но очень информативный. Он буквально говорит: «в dev мы хотим другой title и хотим показывать не только published». Всё остальное берётся из базового файла. Такой подход не только уменьшает ошибки, но и экономит вам силы: вы чаще читаете конфигурацию глазами и реже делаете “археологию” по репозиторию.
4. Финальная конфигурация catalog-service
Теперь нам нужен не просто пример для понимания механики, а спокойный YAML-first baseline catalog-service: один базовый application.yaml, короткие профильные дельты и никаких конкурирующих .properties рядом. Profile groups вроде full-dev могут жить рядом как удобный alias для частых запусков, но сам базовый набор файлов и без них уже полностью рабочий. На такой базе потом легко разносить конфигурацию по внешним файлам и дополнительным locations, не теряя контроль над precedence.
application.yaml — база: структура и общие defaults
Начнём с базового файла. Здесь живёт общая структура app.catalog.*, которая не зависит от окружения. Здесь же можно задать «file-based выбор профиля» через placeholder, если вам это нужно для учебного проекта. Главное — держать выбор профиля в одном понятном месте.
# src/main/resources/application.yaml
spring:
application:
# Имя приложения: обычно одинаковое во всех окружениях
name: "catalog-service"
profiles:
# Выбор профиля снаружи (env var), с безопасным fallback на local
active: "${APP_PROFILE:local}"
app:
catalog:
# Базовый заголовок — дефолт для всех профилей
title: "Course Catalog"
# Общие параметры, которые не должны «плавать» по окружениям без причины
max-featured-count: 4
default-published-only: true
Такой дефолт не спорит с прошлыми способами запуска: SPRING_PROFILES_ACTIVE, -Dspring.profiles.active=... и --spring.profiles.active=... всё равно могут переопределить этот выбор на конкретный старт. И да, app.catalog.* — это наши прикладные ключи: Boot честно положит их в Environment, а смысл им уже даёт код приложения.
application-local.yaml — локальные удобства без утечки в общую конфигурацию
Локальный профиль — это ваша личная территория. Здесь нормально менять порт (чтобы не конфликтовать с другими приложениями), и здесь допустимо сделать более «удобный» default, например показывать и unpublished курсы, если вам нужно видеть весь каталог при разработке.
# src/main/resources/application-local.yaml
server:
# Локальный порт: удобно, но не должно попадать в командный dev/prod
port: 8081
app:
catalog:
# В локальной среде можно явно пометить, что это не dev/prod
title: "Course Catalog (local)"
# Локально разрешаем видеть всё, чтобы удобнее разрабатывать
default-published-only: false
Здесь профильный файл короткий: порт и два отличия. Он не переписывает max-featured-count, не повторяет spring.application.name, не копирует флаги, которые и так одинаковы. Это как хорошо написанный патч: меняет ровно то, что надо.
application-dev.yaml — общий dev-baseline, но без персональных деталей
Dev-профиль — это не «мой ноутбук», а «условная среда разработки команды». Поэтому порт здесь чаще всего не задают (или задают согласованно), а логика остаётся похожей на local: можно показывать больше данных, чем в проде.
# src/main/resources/application-dev.yaml
app:
catalog:
# Dev — командная среда: настройки должны быть общими для всех
title: "Course Catalog (dev)"
default-published-only: false
Обратите внимание: dev и local похожи, но это нормально. Они решают похожие задачи. Разница в том, что local может включать “мой” порт и “мои” удобства, а dev — общий baseline без индивидуальных отличий.
application-prod.yaml — строгие отличия продового режима
Если база уже сформулирована как safe default, prod-слой может быть совсем коротким или вообще не появляться до первого реального продового отличия. Для текущего состояния catalog-service нам достаточно пустого файла: базовые значения уже строгие, а все послабления живут только в local и dev.
# src/main/resources/application-prod.yaml
# Пока пусто: для текущего состояния catalog-service
# safe defaults уже заданы в application.yaml
Если у prod появятся собственные отличия, именно сюда и ляжет эта дельта.
5. Проверяем итоговую картину
Когда вы начинаете работать с профилями и несколькими источниками свойств, появляется новая реальность: конфигурация существует не только как набор файлов, но и как итоговое состояние, которое приложение реально видит через Environment. Сам приём уже знаком: посмотреть, какие профили и значения действительно победили после наложения слоёв. Теперь просто упакуем эту диагностику в маленький ConfigSnapshot, чтобы в проекте была одна аккуратная точка наблюдения, а не россыпь println по разным местам.
Это удобно не только сейчас. Если часть конфигурации потом уедет во внешние файлы, Environment всё равно покажет итоговую картину, а не заставит вспоминать, из какого именно слоя приехало значение.
Компонент ConfigSnapshot: одна строка правды для старта
Сделаем простой компонент. Он не пытается стать «системой конфигурации», он просто собирает короткую строку: title и активные профили. Этого часто достаточно, чтобы быстро понять, почему приложение ведёт себя иначе.
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class ConfigSnapshot {
// Environment — единая точка чтения итоговых свойств после наложения профилей
private final Environment environment;
public ConfigSnapshot(Environment environment) {
this.environment = environment;
}
public String summary() {
// Берём ключевое свойство, которое часто меняется по профилям
String title = environment.getProperty("app.catalog.title", "n/a");
// Active profiles — то, что Boot реально активировал (а не то, что мы «ожидали»)
String profiles = String.join(", ", environment.getActiveProfiles());
// Если профилей нет, считаем, что работаем в «базовом» режиме
return title + " | profiles=" + (profiles.isBlank() ? "base" : profiles);
}
}
Тут важно, что мы не используем никаких будущих механизмов type-safe binding. Мы работаем ровно тем, что у нас уже есть: Environment и getProperty(...).
Подключаем ConfigSnapshot в StartupSummaryRunner
Предположим, что StartupSummaryRunner у вас уже существует (мы вводили runners раньше). Теперь сделаем его чуть полезнее: вместо «просто распечатать профили» печатаем короткий снапшот.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class StartupSummaryRunner implements ApplicationRunner {
// Вкалываем зависимость, которая умеет собрать «итоговую строку»
private final ConfigSnapshot snapshot;
public StartupSummaryRunner(ConfigSnapshot snapshot) {
this.snapshot = snapshot;
}
@Override
public void run(ApplicationArguments args) {
// Печатаем один понятный маркер: сразу видно и title, и активный профиль
System.out.println(snapshot.summary()); // Course Catalog (local) | profiles=local
}
}
Если вы запустите приложение с APP_PROFILE=dev, то увидите другой текст, и это мгновенно подтверждает, что профиль реально активировался, а свойства реально перекрылись. Это маленькая штука, но она экономит время и нервы. А время и нервы, как известно, в IT — два главных невозобновляемых ресурса.
6. Типичные ошибки при работе с профилями
Ошибка №1: держать application.yaml и application.properties как “два равноправных главных файла”.
Обычно это случается случайно: один файл принёс шаблон, другой добавил разработчик «потому что так привычнее». Дальше начинается борьба за приоритеты: вы меняете YAML, но изменения не проявляются, потому что рядом есть .properties с тем же ключом. Если вы выбрали YAML-first подход, лучше удалить .properties и перестать жить в режиме «угадай, какой файл победил».
Ошибка №2: копировать весь базовый конфиг в application-local.yaml / application-dev.yaml / application-prod.yaml.
Это кажется удобным ровно один раз — в момент копирования. Потом начинается сопровождение: вы меняете базу, забываете поменять копии, и получаете различия там, где их не планировали. Profile-файлы должны быть короткими, и хороший ориентир тут простой: если профильный файл стал больше базового — где-то вы свернули не туда.
Ошибка №3: “локальные удобства” утекают в базовый application.yaml.
Классический пример: вы однажды подняли порт на 8081, чтобы не конфликтовать с другим сервисом, и положили это в базовый файл. Потом коллега запускает проект, у него “внезапно” другой порт, хотя он ожидал дефолтный. Локальные вещи (порт, «показывай всё», какие-то временные флаги) должны жить в application-local.yaml, иначе вы превращаете базу в сборник личных предпочтений.
Ошибка №4: задавать spring.profiles.active внутри application-dev.yaml или application-prod.yaml.
Такой конфиг выглядит как попытка сделать самоисполняющуюся магию: «пусть dev-файл сам включает dev». Но логика тут ломается: чтобы Boot применил application-dev.yaml, dev-профиль уже должен быть активен. Поэтому выбор активного профиля держат либо во внешнем запуске (env vars / CLI args), либо в одном понятном месте в базовом файле через placeholder, но не внутри profile-файлов.
Ошибка №5: не проверять итоговое состояние и надеяться, что “и так понятно”.
Когда появляются несколько профилей и несколько источников, ожидания часто расходятся с реальностью. Вы уверены, что активен dev, но на самом деле приложение стартовало с local из fallback. Вы уверены, что title берётся из dev-файла, но он переопределился где-то ещё. Маленький ConfigSnapshot или даже простая печать environment.getActiveProfiles() на старте — это способ перестать «верить» и начать «знать».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ