JavaRush /Курси /Spring Boot /Карта PropertySource

Карта PropertySource

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

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 після старту» не завжди дорівнює «воно точно брало участь у старті». Це не помилка, а особливість самого процесу запуску.

1
Задача
Spring Boot, 14 рівень, 0 лекція
Недоступна
Підсумкове значення з Environment
Підсумкове значення з Environment
1
Задача
Spring Boot, 14 рівень, 0 лекція
Недоступна
Зовнішній файл сильніший за packaged-конфіг
Зовнішній файл сильніший за packaged-конфіг
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ