1. Причины выноса конфига во внешние каталоги
Как только у внешней конфигурации появляется несколько слоёв, одного ответа «сделаем файл optional или обязательным» уже мало. Сразу возникает следующий приземлённый вопрос: где эти файлы и каталоги вообще лежат, как Boot их находит и как не превратить внешний конфиг в новый квест. Вот теперь разбираемся именно с этой топологией.
Пока мы работали с application.yaml внутри src/main/resources, всё было уютно: открыл файл в IDE, поправил, запустил. Но как только сервис перестаёт жить только в IDE и его надо запускать как jar, конфиг хочет жить рядом с приложением, а не внутри него.
Внешние конфиги появляются не потому, что “так модно”, а потому что это самый простой способ разделить роли. Представьте, что у вас есть базовые настройки, которые одинаковы у всех (например, название сервиса и общие флаги), и есть “мелкие подстройки”, которые отличаются у конкретного разработчика на ноутбуке. Если держать всё внутри src/main/resources, то вы либо начинаете коммитить чужие локальные настройки в Git, либо играете в “не забудь откатить перед пушем” (спойлер: забудете).
Когда конфигурация разложена по нескольким внешним каталогам, вы можете получить удобную схему: один каталог — «командная база», второй — «локальные оверрайды», третий — «экспериментальные настройки». Это особенно полезно, если вы хотите иметь несколько независимых “пакетов конфигов” и включать их по ситуации, не переписывая файл, который уже упакован в jar.
Важно удержать мысль: мы сейчас не про “сложную инфраструктуру” и не про “облачные секреты”. Мы про очень приземлённую задачу — не ломать объяснимость конфигурации, когда у вас появляется больше одного внешнего слоя.
2. Директория как локация конфигурации
Когда вы говорите Spring Boot “ищи конфигурацию в этой директории”, вы как будто говорите ему: “Это не склад любых YAML-файлов на случай апокалипсиса. Это место, где лежат файлы с ожидаемыми именами, и ты должен их найти”. И тут часто происходит первая путаница у начинающих.
Директория в spring.config.location или spring.config.additional-location — это не “возьми все *.yaml подряд”. Директория — это место, где Boot будет искать стандартные имена файлов конфигурации. В нашем курсе это, прежде всего, application.yaml и профильные application-{profile}.yaml. То есть если вы добавили внешнюю директорию, но положили туда файл catalog-extra.yaml, то Boot не обязан “догадаться”, что вы имели в виду именно его. Он увидит директорию, посмотрит на стандартные имена — и пойдёт дальше.
Отсюда очень практическое правило: если вы хотите, чтобы файл с произвольным именем участвовал в конфигурации, у вас есть два честных пути. Первый — импортировать его через spring.config.import. Второй — указать его как конкретный файл в spring.config.location или spring.config.additional-location. А вот “просто положить рядом в директорию” — работает только для стандартных application*.yaml.
Ещё одна деталь: когда вы указываете директорию, путь должен заканчиваться /. Это не эстетика и не придирка, а способ сказать Boot: “это каталог, а не файл”. Иначе вы рискуете попасть в ситуацию “вроде указал путь, а Boot ведёт себя не так”, потому что он интерпретировал его как точный файл.
Ниже — маленькая шпаргалка, которая помогает не путаться:
| Что вы указываете | Пример | Как Boot это понимает |
|---|---|---|
| Директорию | file:./config/ | Ищи внутри стандартные application*.yaml |
| Конкретный файл | file:./config/catalog-extra.yaml | Загрузи именно этот файл “как есть” |
| Classpath-ресурс | classpath:catalog-data.yaml | Найди ресурс внутри jar (или в classpath IDE) |
3. Дефолтный поиск и ./config/*/
На этом месте полезно на секунду остановиться и сказать: Spring Boot не ждёт, пока вы напишете магические параметры. Он уже “по умолчанию” пытается вам помочь и ищет конфигурацию в нескольких стандартных местах. И именно тут часто случается сюрприз уровня “почему оно вообще нашло мой файл?!”.
Одно из самых практических стандартных мест — внешняя папка ./config/ рядом с jar (или рядом с проектом, если вы запускаете через IDE/Gradle). Boot умеет искать там application.yaml и application-{profile}.yaml. И, что важно для нашей лекции, Boot умеет смотреть ещё и в непосредственные подкаталоги: условно ./config/*/. Это значит, что структура с несколькими внешними слоями может работать даже без дополнительных параметров, если вы придерживаетесь ожидаемой “географии”.
Например, такая раскладка вполне жизнеспособна:
./config/
├── base/
│ └── application.yaml
└── override/
└── application.yaml
Слово base и override здесь — просто имена каталогов. Сам факт, что они лежат внутри ./config/, уже делает их кандидатами на участие в конфигурации, потому что Boot готов сканировать ./config/*/.
И вот здесь важно не перескочить к неверному выводу “значит, можно складывать конфиг как угодно”. Нет. Это работает только для одного уровня вложенности: Boot смотрит на непосредственные подкаталоги, а не на “дерево каталогов любой глубины”. Если вы сделаете ./config/base/dev/application.yaml, то это уже не “непосредственный подкаталог”, и такой файл не будет найден через ./config/*/ поиск.
Чтобы лучше почувствовать границу, можно мысленно представить, что Boot делает что-то вроде: “в папке config/ перечисли папки первого уровня и в каждой попробуй найти стандартные application*.yaml”. Он не превращается в файловый поисковик уровня “найди мне любой yaml, пожалуйста”.
4. Wildcard-локации: синтаксис и порядок
Wildcard-локации — это способ сказать Boot: “вот корневая директория, а дальше посмотри во всех её подкаталогах первого уровня”. Это удобно, когда вы не хотите перечислять каждую папку вручную или когда набор папок меняется (например, кто-то добавил ещё один слой конфигурации), а вы хотите, чтобы Boot автоматически его подхватил.
В Spring Boot wildcard для конфигурационных локаций — это * внутри пути, но с несколькими очень жёсткими ограничениями. Для директории wildcard должен заканчиваться на */, а сам путь может содержать только одну звёздочку. То есть file:./config/*/ — норм, а “давайте я сделаю file:./config/**/ как в некоторых других инструментах” — не норм, и вы получите не то поведение, которое ожидаете.
Ещё одно важное ограничение: wildcard-локации работают именно для внешних директорий (file:), а не для classpath: ресурсов. Это логично: classpath — это не всегда обычная файловая система (в jar внутри архива тоже “файлы”, но не всегда перечисляемые так же, как в OS). Boot не обязан уметь “пробежаться по classpath и найти всё по маске”.
Для стандартного ./config/*/ такая команда часто даже избыточна: Boot и так умеет смотреть туда по умолчанию. Явная запись нужна здесь не потому, что без неё wildcard «не работает», а чтобы увидеть сам синтаксис маски и понять, как этот приём применять к нестандартному корню поиска.
Вот пример, как выглядит запуск с wildcard в качестве дополнительной локации:
./gradlew bootRun --args="--spring.config.additional-location=optional:file:./config/*/"
Что здесь происходит по смыслу: стандартный поиск сохраняется, а Boot дополнительно смотрит во все непосредственные подкаталоги ./config/. Если папки нет, благодаря optional: старт не ломается.
Теперь — “алфавитная магия”, которая на самом деле не магия, а правило. Когда wildcard раскрывается (то есть Boot превращает ./config/*/ в конкретный список директорий), эти директории сортируются по алфавиту. Это значит, что порядок слоёв может определяться тем, как вы назвали папки. И если вы не осознаёте это правило, то вашим главным архитектором внезапно становится… алфавит.
Чтобы показать это на пальцах, представим, что у нас есть такие каталоги:
./config/
├── 01-base/
│ └── application.yaml
└── 99-override/
└── application.yaml
Алфавитный порядок тут очевиден, и вы почти не рискуете перепутать “что раньше, что позже”. А вот если папки будут называться “tmp”, “new”, “final2”, то вы получите порядок, который вроде бы детерминированный, но выглядит как загадка.
Небольшая схема того, что происходит при wildcard, хорошо укладывается в такой поток:
flowchart TD A["--spring.config.additional-location=file:./config/*/"] --> B["Wildcard раскрывается в список директорий"] B --> C["Список сортируется по алфавиту"] C --> D["Boot ищет application*.yaml в каждой директории"] D --> E["Собирается итоговая конфигурация (last wins)"]
Обратите внимание на последний шаг: при наложении значений работает принцип “последний победил”. Поэтому порядок директорий — это не косметика, а фактически часть конфигурационной архитектуры.
5. Порядок слоёв: когда помогает wildcard
Wildcard — штука удобная, но она же может стать ловушкой. С одной стороны, он снимает необходимость перечислять каждый подкаталог руками. С другой стороны, он добавляет “скрытую зависимость” от порядка, который задаётся именами папок. И вот тут начинается взрослая жизнь конфигурации: либо вы принимаете этот порядок как часть соглашений, либо вы выбираете более явный вариант.
Если порядок реально важен, то полагаться на алфавит — решение на троечку: вроде работает, но всегда найдётся момент, когда кто-то добавит папку aaa-test и внезапно “перестроит” вашу конфигурационную пирамиду. Поэтому хорошая инженерная привычка — считать wildcard инструментом “для удобства”, а не инструментом “для критически важного порядка”.
Когда wildcard действительно хорош: у вас есть набор дополнительных “мягких” слоёв, где порядок не настолько критичен, или вы уверены, что имена папок будут строго подчиняться соглашению. Например, если договорились, что все каталоги имеют префиксы 01-, 02-, 03- и это контролируется code review (да, конфиг тоже надо ревьюить, он умеет ломать системы ничуть не хуже кода).
Когда wildcard начинает мешать: вы хотите гарантировать приоритет так, чтобы он читался прямо из команды запуска. В этом случае перечисление локаций явно обычно спокойнее для психики:
./gradlew bootRun --args="--spring.config.additional-location=optional:file:./config/base/,optional:file:./config/override/"
Здесь порядок понятен человеку без “догадок”. Даже если кто-то создаст ./config/aaa-new/, оно не начнёт участвовать в конфигурации само по себе.
И ещё один момент, который полезно проговорить словами: wildcard смотрит только на непосредственные подкаталоги. Он не ищет по дереву, не делает рекурсию, не сканирует “на два уровня вниз”. Поэтому схемы вида “положу конфиги в config/envs/local/application.yaml” не заработают через wildcard автоматически. Это не хорошо и не плохо — это просто граница механизма. Boot делает ровно то, что вы попросили, без попыток быть экстрасенсом.
6. Мини-сценарий: catalog-service
Сейчас мы соберём всё в маленький пример на нашем catalog-service, чтобы тема не осталась “про абстрактные папки”. Мы не будем строить идеальную финальную схему (это как раз следующая лекция дня), а сделаем узкий сценарий: два внешних слоя конфигурации через подкаталоги и проверка, какое значение в итоге увидел сервис.
Представим, что в src/main/resources/application.yaml у нас есть базовый заголовок каталога (просто чтобы было что переопределять), а внешний конфиг должен его подменить. Тогда мы заводим рядом с проектом такую структуру:
./config/
├── 01-base/
│ └── application.yaml
└── 99-override/
└── application.yaml
Содержимое файлов делаем минимальным и намеренно конфликтующим:
# ./config/01-base/application.yaml
# Базовый слой: задаём значение "по умолчанию", которое могут перекрыть оверрайды
app:
catalog:
title: "Spring+ Catalog (base)"
# ./config/99-override/application.yaml
# Слой оверрайда: перекрывает одноимённое значение из base
app:
catalog:
title: "Spring+ Catalog (override)"
Теперь запускаем приложение, явно добавив wildcard-локацию как дополнительный слой (и делая её optional, чтобы на чужом компьютере запуск не падал):
./gradlew bootRun --args="--spring.config.additional-location=optional:file:./config/*/"
Да, для стандартного ./config/*/ такой запуск можно было бы и не прописывать, но здесь локация задана явно, чтобы фокус был на порядке слоёв, а не на догадках про дефолтный поиск.
Чтобы не гадать, какое значение победило, проще всего добавить маленькую диагностическую точку. В рамках модуля конфигурации допустимо использовать Environment и вывести значение в консоль (да, позже мы будем логировать иначе, но сейчас нам нужен быстрый “рентген”):
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) {
this.env = env;
}
@Override
public void run(ApplicationArguments args) {
// ApplicationRunner вызывается после поднятия контекста — удобно для "быстрой диагностики"
// Ключ берём ровно такой, как в YAML: app.catalog.title
System.out.println(env.getProperty("app.catalog.title"));
// Если всё подключено правильно, победит более поздний слой (99-override)
// Spring+ Catalog (override)
}
}
Результат здесь важен не как “красивый текст”, а как подтверждение правил. Если у вас override — более поздний слой, то именно он и должен “победить”. А если внезапно победил base, это почти всегда означает, что вы ошиблись с порядком локаций, именами каталогов или вообще не подключили внешний слой так, как думали.
И последний нюанс, который стоит держать в голове в этом сценарии: когда вы подключаете директорию как локацию, Boot будет искать стандартные application*.yaml. Поэтому если вы решите назвать файл catalog-extra.yaml, он не начнёт работать “сам по себе” только из-за того, что лежит в подкаталоге ./config/.../. Для произвольного имени нужен либо import, либо точная file-локация.
7. Типичные ошибки при работе с локациями
В этой теме ошибки чаще всего не “красные” и очевидные, а “скользкие”: приложение стартует, но ведёт себя не так, как вы ожидали. И это особенно коварно, потому что мозг любит думать: “Раз стартануло — значит конфиг применился”. На практике конфиг мог не примениться вообще, и вы просто запускаете сервис на старых значениях.
Ошибка №1: ожидать, что директория подхватит любой YAML-файл.
Частая логика новичка: “Я добавил --spring.config.additional-location=file:./config/, значит Boot прочитает всё, что там лежит”. Но директория — это не “прочитай всё подряд”, это “ищи стандартные application*.yaml”. Если вы положили туда catalog-data.yaml или catalog-extra.yaml, он может не загрузиться. Для произвольного имени нужен либо spring.config.import, либо точное указание файла как локации.
Ошибка №2: забыть, что wildcard работает только по первому уровню подкаталогов.
Очень хочется сделать аккуратную структуру “config/envs/local/application.yaml”, но wildcard file:./config/*/ смотрит только на непосредственные подкаталоги config/. Он не рекурсивный. В итоге Boot честно ничего не находит, а вы потом полчаса смотрите в YAML и думаете, что там опечатка.
Ошибка №3: пытаться использовать wildcard для classpath:.
Конфигурация внутри jar — это не файловая система, и Boot не обязан “разворачивать маски” в classpath. Поэтому конструкции вида classpath:config/*/ — почти гарантированный путь к разочарованию. Wildcard-локации — это инструмент для внешних директорий (file:), то есть для того, что лежит рядом с jar/project.
Ошибка №4: полагаться на алфавитический порядок, не осознавая его.
Когда wildcard раскрывается и сортируется по алфавиту, порядок слоёв зависит от имён папок. Если у вас папки называются случайно, то и приоритет получается “случайным на вид”. В какой-то момент кто-то добавит новую папку, и итоговые значения вдруг поменяются. Если порядок важен — перечисляйте локации явно или вводите строгие соглашения об именовании.
Ошибка №5: забыть / у директории и получить “почему оно не ищет файлы”.
Для Boot разница между file:./config и file:./config/ принципиальна. Во втором случае это каталог, и Boot начинает искать application*.yaml внутри. В первом — вы как будто указали файл, и поведение становится другим. Это мелочь, но она регулярно ломает запуск, особенно когда команда запуска копируется между людьми и платформами.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ