1. Карта джерел конфігурації
Уявіть ситуацію: ви чесно змінюєте app.catalog.title у application.yaml, зберігаєте файл, запускаєте сервіс… а заголовок у відповідях усе одно старий. Перша думка — «IDE не зберегла», друга — «Spring знову магія», третя — «я точно програміст, чи просто щасливий друкар YAML?». Насправді проблема майже завжди буденна: значення береться не з того місця, куди ви дивитеся.
Spring Boot уміє збирати конфігурацію з кількох джерел одночасно. Якщо один і той самий ключ задано в кількох місцях, застосунок усе одно побачить рівно одне підсумкове значення. Щоб не лікувати симптоми, нам потрібна нормальна ментальна модель: які бувають джерела, як вони складаються і чому одне значення перемагає інше.
PropertySource і джерела властивостей
Коли ви чуєте «конфігурація Spring Boot», мозок зазвичай малює один файл: application.yaml. Це нормально, ми всі так починали. Але в термінах Spring це надто вузько. PropertySource — це просто «місце, звідки можна взяти значення за ключем». Це може бути YAML-файл, змінна середовища ОС, параметр JVM, аргумент командного рядка або навіть JSON-блок, захований у змінній середовища. Файл — лише один із варіантів.
Важливо вловити просту думку: Boot не читає одне-єдине правильне джерело, він збирає багатошаровий пиріг. Кожен шар — це PropertySource. Шари накладаються один на один, і зрештою отримуємо одну підсумкову картину світу, з якої вже читає ваш код. Тому запитання «у якому файлі лежить значення» часто неправильне. Правильне запитання звучить так: «яке підсумкове значення бачить застосунок у runtime, і яке джерело його перекрило?».
Щоб відчути це руками, корисно розглядати конфігурацію як величезну Map<String, String>, але зібрану з різних підкарт. Spring Boot виступає в ролі акуратного збирача: він бере ці підкарти, вибудовує їх у визначеному порядку і каже: «якщо ключ трапляється кілька разів — переможе той, хто стоїть вище».
2. precedence і пріоритети джерел
Слово precedence у цій темі звучить страшніше, ніж є насправді. Це не «таємне магічне налаштування», а звичайний порядок пріоритетів: хто сильніший, той і правий. Якщо один і той самий ключ присутній у кількох джерелах, Boot не влаштовує демократію, не проводить голосування і не питає поради у вашого кота. Він бере значення з більш пріоритетного джерела — і все.
Найзручніше думати про precedence як про стопку прозорих плівок. Унизу лежить базовий шар — значення за замовчуванням. Вище накладаються шари, які уточнюють поведінку. Якщо нагорі намальовано нове значення для того самого ключа, нижнє просто перекривається. Це нормально і навіть корисно: ви можете залишити добрий дефолт у застосунку, всередині jar, а під час запуску на іншій машині перекрити окремі значення без повторного збирання.
Ось спрощена схема. Ми поки що свідомо не заглиблюємося в точний порядок усіх рідкісних джерел — нам потрібна карта, а не довідник на 40 сторінок.
flowchart TB
A["Упакована конфігурація (усередині застосунку)"] --> E["Spring Environment (підсумкові значення)"]
B["Зовнішня конфігурація (поруч із запуском)"] --> E
C["Переозначення під час запуску (параметри запуску)"] --> E
E --> F["Ваш код читає env.getProperty(...)"]
Тут ключова ідея не в тому, що файли погані, а параметри запуску — хороші. Ідея в тому, що у конфігурації є шари відповідальності. Файл усередині застосунку зазвичай задає базовий дефолт. Зовнішній файл — налаштування для конкретного середовища. А параметри запуску — разове переозначення тут і зараз.
Якщо ви зрозумієте цей принцип, більшість конфігураційних містичних помилок раптово перестануть бути містичними. Вони стануть нудними. А нудне — це прекрасно, тому що нудне виправляється за інструкцією.
3. Вбудована і зовнішня конфігурація
Тепер про те, що найчастіше реально трапляється в проєктах: конфігурація всередині застосунку і конфігурація зовні. На рівні нашої ментальної моделі досить пам’ятати два слова: вбудована (packaged) і зовнішня (external).
Вбудована конфігурація — це те, що лежить у вашому проєкті в src/main/resources і їде всередину jar. Наприклад, src/main/resources/application.yaml. Коли ви збираєте застосунок, цей файл стає частиною артефакту. Це зручно, тому що в застосунку з’являється вбудований дефолт: він може стартувати навіть без зовнішніх файлів.
Зовнішня конфігурація — це те, що лежить поруч із запуском застосунку, умовно «файл поруч із jar» або в типовій папці конфігурації. Ідея проста: ви не хочете повторно збирати jar, щоб змінити заголовок, порт або прапорець режиму обслуговування. Тому ви підкладаєте зовнішній файл, і Boot використовує його як сильніший шар порівняно з вбудованим.
Виглядає це максимально буденно: один і той самий ключ може трапитися у двох місцях, і це не помилка, якщо ви чітко розумієте, який із них — дефолт, а який — override.
# вбудована: src/main/resources/application.yaml
app:
catalog:
title: "Каталог із jar"
# якщо ключ повторюється, переможе більш пріоритетний шар (наприклад, зовнішній файл)
# зовнішня: умовно ./config/application.yaml поруч із запуском
app:
catalog:
title: "Каталог із зовнішнього файлу"
Якщо застосунок побачив "Каталог із зовнішнього файлу", це не означає, що він проігнорував ваш application.yaml. Він його прочитав. Просто зверху лежав шар, який перекрив значення.
І тут з’являється важлива дисципліна: один і той самий ключ у кількох файлах допустимий, але лише якщо ви можете пояснити, навіщо. Якщо не можете — це вже не layered config, а конфігураційна археологія: за три дні ви займатиметеся розкопками, зʼясовуючи, де ж справжнє значення.
4. Environment: підсумкова конфігурація
Слово Environment у Spring майже гарантовано викликає плутанину в новачків, бо в голові вже є environment variables операційної системи. Так, ОС теж надає змінні середовища. Але Spring Environment — це інше. Це Spring-об’єкт, який зберігає підсумок збирання всіх PropertySource з урахуванням precedence. Тобто це все, що застосунок знає про свої налаштування, в одному місці.
Ця ідея сильно спрощує життя: вашому коду не потрібно знати, звідки прийшло значення. З файла? Із змінної середовища? З аргументу запуску? Неважливо. Код читає Environment — і отримує фінальну відповідь. А коли щось працює не так, ви не вгадуєте за файлами, а йдете в Environment і перевіряєте, що насправді бачить runtime.
У catalog-service ми можемо, в навчальних цілях, додати маленький раннер, який виводить пару ключів і показує, що перемогло. Зараз ми використовуємо System.out.println, тому що окрема тема логування буде пізніше, а сьогодні нам важливіше побачити сам принцип.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
class PropertyProbeRunner implements ApplicationRunner {
private final Environment environment;
PropertyProbeRunner(Environment environment) {
// Spring сам інʼєктує Environment, який уже зібрав усі PropertySource з урахуванням пріоритетів
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
// Читаємо підсумкове значення: неважливо, чи прийшло воно з YAML, env vars або аргументу запуску
// "<missing>" — явний дефолт, щоб відрізняти "ключ відсутній" від "ключ є, але значення порожнє"
String title = environment.getProperty("app.catalog.title", "<missing>");
// Для демонстрації принципу використовуємо System.out.println (логування буде окремою темою)
System.out.println("app.catalog.title = " + title); // app.catalog.title = Spring + Каталог
}
}
Цього одного тимчасового probe-runner для таких перевірок цілком достатньо. Далі, якщо захочете подивитися інший ключ або порівняти інший канал запуску, простіше змінювати вміст run(...), ніж тримати в проєкті колекцію схожих стартових допоміжних класів: інакше запуск швидко перетворюється на шум.
Зверніть увагу на дві деталі. По-перше, ми не парсимо YAML вручну і не читаємо файли через Files.readString(...). По-друге, ми використовуємо дефолт "<missing>", щоб явно відрізняти ситуацію «ключ не знайдено» від ситуації «ключ знайдено, але значення порожнє». Це маленька звичка, але вона дуже рятує нерви.
Ще одна корисна можливість Environment: він уміє просте перетворення типів. Поки ми не прив’язуємо конфігурацію до окремих Java-об’єктів, але навіть на базовому рівні можна попросити Spring повернути значення як Integer або Boolean.
import org.springframework.core.env.Environment;
class SimpleReads {
private final Environment env;
SimpleReads(Environment env) {
// Environment тут — той самий підсумковий "знімок" конфігурації, зібраний з усіх джерел
this.env = env;
}
void show() {
// Spring спробує перетворити рядкове значення на потрібний тип
// Третій аргумент — дефолт, який буде використано, якщо ключ не знайдено
Integer limit = env.getProperty("app.catalog.max-featured-count", Integer.class, 4);
Boolean maintenance = env.getProperty("app.catalog.maintenance-mode", Boolean.class, false);
System.out.println("limit = " + limit); // limit = 4
System.out.println("maintenance = " + maintenance); // maintenance = false
}
}
Це все ще сирий спосіб роботи з конфігурацією, але для сьогоднішньої теми він ідеальний: він підкреслює, що головний об’єкт, який бачить застосунок, — Environment, а не окремий файл.
5. Мінікейс: один ключ у різних джерелах
Давайте зберемо в голові максимально типовий і максимально життєвий кейс: один ключ app.catalog.title задано в кількох місцях. Наприклад, у src/main/resources/application.yaml ви залишили базове значення, тому що проєкт має стартувати «з коробки». Потім ви запускаєте застосунок на іншому комп’ютері й хочете інший заголовок — підкладаєте зовнішній конфіг. А потім, у момент діагностики, робите разовий запуск із параметром, бо «треба перевірити ось просто зараз, не чіпаючи файлів».
Ми зараз не заглиблюємося в конкретний синтаксис і точний порядок усіх каналів запуску — це окремі теми наступних лекцій. Тут важливий сам принцип: ключ один, джерел кілька, підсумкове значення одне.
Схематично це можна подати так:
| Де задано значення | Приклад значення | Роль |
|---|---|---|
| Вбудована конфігурація (усередині jar) | Каталог із jar | базовий дефолт, щоб проєкт стартував |
| Зовнішня конфігурація (поруч із запуском) | Каталог із зовнішнього файла | налаштування для конкретного середовища або машини |
| Переозначення під час запуску (з командного рядка) | Каталог із командного рядка | разове коригування «на цей запуск» |
І саме тому під час налагодження завжди корисно ставити собі запитання не «у якому файлі в мене написано…», а «який підсумковий шар зараз переміг?». Бо ви можете дивитися в вбудований application.yaml до посиніння, але якщо зверху є override, ваш погляд буде абсолютно чесним, але марним.
У реальному проєкті це і є причина, чому людина говорить: «Я ж змінив!», а застосунок відповідає: «Так, я бачив. І все одно зробив по-своєму». Boot не шкідничає — він просто дотримується правила precedence.
6. Читабельна конфігурація за наявності багатьох джерел
Коли ви вперше дізнаєтеся, що конфігурація може приходити звідусіль, є спокуса почати користуватися цим у стилі «ну раз можна, то давайте покладемо одне й те саме у пʼять місць — про всяк випадок». Це швидко приводить до стану, коли конфіг начебто є, але керувати ним неможливо: будь-яка зміна перетворюється на ворожіння.
Здорова стратегія починається з простого розподілу ролей. Вбудована конфігурація всередині проєкту має бути місцем, де лежать розумні дефолти й те, що робить застосунок самодостатнім. Зовнішня конфігурація потрібна, коли ви хочете змінювати поведінку без повторного збирання і без правок вихідного коду. Параметри запуску корисні для діагностики та разових перемикань, коли ви взагалі не хочете редагувати файли.
Якщо тримати це розділення в голові, ви інтуїтивно перестаєте дублювати ключі «просто тому, що можна». Ви починаєте ставити запитання: «це значення — дефолт чи override?». І конфігурація перестає бути купою сміття.
Ще одна маленька, але дуже практична думка: коли ви налагоджуєте дивну поведінку, не намагайтеся відразу знайти «той самий файл». Краще знайти ключ, а потім перевірити, яке значення він реально приймає в Environment. Це навіть психологічно простіше: сперечатися з реальністю важко, а Environment — це реальність у форматі String.
Звідси виростає і наступне практичне запитання: яким каналом запуску ви взагалі перекриваєте цей базовий дефолт — зовнішньою змінною середовища, -D чи --key=value. Щойно ви починаєте перевіряти це не за файлами, а за підсумковим значенням у Environment, половина магії одразу зникає.
7. Типові помилки: ментальна модель джерел властивостей
У цій темі помилки рідко виглядають як «не компілюється». Частіше це помилки мислення: застосунок працює, але «не слухається», і ви починаєте думати, що він ігнорує ваші налаштування. На практиці він просто чесно дотримується precedence і збирає значення з кількох джерел. Давайте зафіксуємо найчастіші граблі, щоб ви наступали на них рідше й свідоміше.
Помилка №1: вважати application.yaml єдиним джерелом істини.
Новачки часто мислять так: «якщо я бачу значення в YAML, значить застосунок зобов’язаний його використовувати». Але Boot бачить не один YAML, а набір джерел. Тому правильна звичка — розділяти «де значення оголошено» і «яке значення стало підсумковим». Підсумок завжди живе в Environment, а не у файлі, відкритому в IDE.
Помилка №2: плутати змінні середовища ОС і Spring Environment.
Змінні середовища операційної системи — це одне з джерел. Spring Environment — це об’єкт, який містить підсумок, уже зібраний із різних джерел. Якщо змішати ці поняття, ви починаєте очікувати від Environment поведінки термінала, а від термінала — поведінки Spring. Закінчується це зазвичай тим, що ви починаєте «перейменовувати змінні, поки не запрацює», замість того щоб перевіряти підсумкове значення.
Помилка №3: діагностувати поведінку за одним файлом, ігноруючи шари.
Найпоширеніший сценарій конфлікту: ключ є у вбудованій конфігурації, але зверху його перекрили. Ви дивитеся у вбудований файл і не розумієте, чому поведінка не змінюється. Лікується це просто: у разі проблеми з конфігурацією спершу зафіксуйте точний ключ і перевірте його підсумкове значення під час виконання, а не влаштовуйте чемпіонат із читання YAML очима.
Помилка №4: дублювати ключі в кількох місцях без ролі та домовленостей.
Іноді люди «про всяк випадок» копіюють один і той самий ключ у кілька файлів: у application.yaml, у якийсь зовнішній конфіг, у IDE Run Configuration. Спочатку все якось працює, а потім будь-яка зміна починає поводитися непередбачувано. У багатошаровій конфігурації ключ може траплятися кілька разів, але кожне його входження має мати зрозумілу роль: дефолт, зовнішнє перекриття або разовий запуск.
Помилка №5: думати, що раз значення видно після старту, значить воно завжди впливало на поведінку старту.
Це тонкий момент. Поки достатньо запам’ятати принцип: деякі налаштування важливі дуже рано, і в них є фактор часу. Тому фраза «я вивів значення через Environment після старту» не завжди дорівнює «воно точно брало участь у старті». Це не помилка, а особливість самого процесу запуску.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ