JavaRush /Курси /Spring Boot /Зовнішні каталоги й wildcard-локації

Зовнішні каталоги й wildcard-локації

Spring Boot
Рівень 16 , Лекція 3
Відкрита

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 усередині архіву теж «файли», але не завжди перелічувані так само, як в ОС). 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["Складається підсумкова конфігурація (останній перемагає)"]

Зверніть увагу на останній крок: за накладання значень працює принцип «останній перемагає». Тому порядок каталогів — це не косметика, а фактично частина конфігураційної архітектури.

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
# Шар перезапису: перекриває однойменне значення з базового шару
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/проєкт.

Помилка №4: покладатися на алфавітний порядок, не усвідомлюючи його.
Коли wildcard розкривається і сортується за алфавітом, порядок шарів залежить від імен папок. Якщо ваші папки названі випадково, то й пріоритет виглядатиме випадковим. У якийсь момент хтось додасть нову папку — і підсумкові значення раптом зміняться. Якщо порядок важливий, перелічуйте локації явно або вводьте суворі домовленості щодо іменування.

Помилка №5: забути / у каталогу й отримати «чому воно не шукає файли».
Для Boot різниця між file:./config і file:./config/ принципова. У другому випадку це каталог, і Boot починає шукати application*.yaml усередині. У першому — ви ніби вказали файл, і поведінка стає іншою. Це дрібниця, але вона регулярно ламає запуск, особливо коли команду запуску копіюють між людьми та платформами.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ