1. Профілі в Spring Boot
Коли проєкт маленький, здається, що конфігурація — це щось на кшталт «одного рядка, який можна підправити й забути». Але щойно ви додаєте реальні можливості, наприклад вкладення з локальним сховищем, раптом виявляється, що один і той самий код хочеться запускати по-різному: комусь потрібно зберігати файли в ./data, комусь — у ./tmp, а на CI взагалі не можна писати будь-куди. І ось тут починається класична драма: «чому в мене працює, а в тебе — ні».
Уявіть, що ви написали LocalAttachmentStorage, який зберігає файли на диск. Локально вам хочеться, щоб файли не засмічували репозиторій, тож ви кладете їх у ./tmp/attachments. В іншому середовищі ви хочете зберігати їх у ./data/attachments, бо ця директорія потрапляє до резервної копії або просто зручніша. API при цьому не змінюється: ті самі ендпоїнти, ті самі DTO, ті самі статуси. Змінюються лише «де лежать файли» і «які ліміти на завантаження».
Якщо розв’язувати задачу «в лоб», ви швидко прийдете до поганих варіантів: правити application.yml вручну перед кожним запуском, тримати три різні файли й копіювати їх один в один, комітити шляхи, залежні від машини, або взагалі повертати хардкод у код — «ну щоб точно працювало». Профілі в Spring Boot придумали саме для того, щоб ви могли сказати: «є базові значення, а для конкретного режиму запуску я додаю невелике перевизначення, яке змінює лише те, що потрібно».
Профіль як «патч» до application.yml
Слово «профіль» у новачків часто викликає неправильне уявлення: ніби це «ще один проєкт» або окремий режим роботи застосунку, де все інакше. На практиці профіль у Spring Boot — це набагато простіше й корисніше: це спосіб покласти поверх базових налаштувань додатковий шар, який перевизначає деякі значення. Зручна метафора — «патч» або «наклейка поверх базової етикетки».
Базовий application.yml — це костюм за замовчуванням застосунку: акуратний, нейтральний, без прив’язки до конкретної машини. Профіль local — це ваш «домашній халат»: у ньому зручно налагоджувати, можна збільшити ліміти, зробити директорію простішою, а логи — балакучішими. Але це все той самий об’єкт, той самий застосунок, ті самі ендпоїнти. Ми не змінюємо профілем контракт API; ми змінюємо лише «в якому взутті бігти» і «яку куртку вдягнути».
Технічно Spring Boot розв’язує дві задачі: він має зрозуміти, який профіль активний, і завантажити додаткові налаштування, що належать до цього профілю. Профіль може бути один (local), може бути кілька (local,debug), але в навчальному проєкті ми тримаємо все максимально просто: базовий application.yml задає значення за замовчуванням для проєкту, application-local.yml — локальне перевизначення, а env vars і аргументи запуску залишаються для машинозалежних налаштувань.
2. Базовий application.yml і профільний application-local.yml
Щоб локальне перевизначення не перетворювалося на окремий конфігураційний світ, для Task Tracker API тримаємо одну канонічну схему: базовий application.yml задає поведінку проєкту за замовчуванням, а application-local.yml змінює лише те, що потрібно локально. Так одразу видно, де базовий рівень репозиторію, а де починається зручність конкретного запуску.
Ось так може виглядати ваш базовий application.yml, який комітиться в репозиторій як єдине джерело правди для поведінки за замовчуванням:
# application.yml
spring:
mvc:
problemdetails:
# Базове налаштування: вмикаємо ProblemDetail, щоб помилки мали однаковий вигляд
enabled: true
servlet:
multipart:
# Базові ліміти (спільні для всіх, якщо їх не перевизначили)
max-file-size: 10MB
max-request-size: 12MB
app:
attachments:
# Типова директорія, придатна для «загального» запуску
storage-dir: ./data/attachments
А ось так — профільний файл для local запуску:
# application-local.yml
spring:
servlet:
multipart:
# Локально можна збільшити ліміти, щоб вони не заважали під час розробки
max-file-size: 20MB
max-request-size: 25MB
app:
attachments:
# Локально зберігаємо в tmp, щоб не захаращувати робочу директорію проєкту
storage-dir: ./tmp/attachments
Це читається просто: «за замовчуванням зберігаємо вкладення в ./data/attachments, а локально — в ./tmp/attachments; базові ліміти 10MB/12MB, локально — 20MB/25MB». При цьому ви не копіюєте весь базовий конфіг у профільний файл, а перевизначаєте лише те, що справді відрізняється.
На практиці команди часто роблять так: application.yml комітиться завжди, а application-local.yml може або комітитися, або бути особистим і лежати в .gitignore. У навчальному проєкті частіше зручно комітити спільний application-local.yml, а зовсім особисті речі задавати через змінні середовища — до цього ми зараз і підійдемо.
Той самий патч можна записати й усередині одного application.yml через --- і spring.config.activate.on-profile. Spring Boot це підтримує, але для поточного проєкту окремий application-local.yml читається простіше: базовий знімок і локальне перевизначення не змішуються в одному файлі.
3. Запуск і перевизначення
Коли схема вже зафіксована, далі все доволі нудно — і це тут плюс. Ми просто активуємо local під час запуску, а якщо якомусь середовищу потрібне ще одне приватне перевизначення, додаємо змінну середовища поверх профілю.
Увімкнення профілю під час запуску
Профілі стають корисними лише тоді, коли ви вмієте керувати ними без шаманства. Гарна новина: увімкнути профіль у Spring Boot можна кількома простими способами, і всі вони зводяться до одного ключа — spring.profiles.active. Ви обираєте один, найзручніший для вашого режиму роботи, і не стрибаєте між десятьма варіантами «залежно від настрою».
Найзручніший спосіб — через конфігурацію запуску в IDE: в IntelliJ IDEA ви додаєте змінну середовища SPRING_PROFILES_ACTIVE=local або вказуєте активний профіль у відповідному полі. Це зручно тим, що все видно одразу: відкрили Run Configuration — побачили, який профіль увімкнено.
Якщо ви запускаєте через Gradle, то можна передати аргументи в bootRun. У навчальному проєкті це часто виглядає так:
# Запуск застосунку через Gradle з явним увімкненням профілю
./gradlew bootRun --args='--spring.profiles.active=local'
Якщо ви запускаєте вже зібраний jar, логіка така сама, тільки аргумент іде процесу JVM застосунку:
# Запуск jar з активним профілем local
java -jar build/libs/task-tracker.jar --spring.profiles.active=local
Корисна практична порада: коли ви вперше налаштовуєте профілі, дуже зручно на старті застосунку виводити активні профілі в лог, щоб не гадати: «а точно воно увімкнулося?». У навчальному проєкті можна зробити невеликий компонент-логер. Він не про бізнес-логіку, тож йому місце в конфігураційному або інфраструктурному шарі.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class ActiveProfilesLogger implements ApplicationRunner {
private final Environment env;
public ActiveProfilesLogger(Environment env) {
// Environment — джерело конфігурації, з нього можна читати активні профілі
this.env = env;
}
@Override
public void run(ApplicationArguments args) {
// Друкуємо профілі, щоб під час старту не гадати, що реально увімкнулося
System.out.println("Активні профілі: " + String.join(",", env.getActiveProfiles()));
// Приклад очікуваного виводу:
// Активні профілі: local
}
}
Так, System.out.println — не найкрасивіший «бойовий» підхід, але в навчальному проєкті він якраз допомагає миттєво побачити результат. Згодом ви, звісно, будете логувати через повноцінний логер, але сенс залишиться тим самим: спочатку переконайтеся, що профіль увімкнено, і лише потім сперечайтеся з YAML.
Перевизначення через змінні середовища
Профілі розв’язують задачу «у нас є передбачуваний локальний режим», але не розв’язують задачу «у кожного розробника своя машина». Саме тут у гру входять перевизначення через змінні середовища. Їхня головна перевага: ви можете змінити значення взагалі не чіпаючи репозиторій. Це ідеальний спосіб сказати: «у моєму середовищі сховище лежить в іншому місці, і це нормально».
Spring Boot уміє підхоплювати значення із env vars і мапити їх на властивості. Корисне практичне правило: крапки й дефіси перетворюються на підкреслення, а ім’я — на верхній регістр. Тому app.attachments.storage-dir перетворюється на APP_ATTACHMENTS_STORAGE_DIR, а spring.servlet.multipart.max-file-size перетворюється на SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE.
Ось приклад типового запуску, де ми активуємо local профіль і водночас перевизначаємо директорію сховища та ліміт файлу:
# 1) Увімкнемо профіль local
export SPRING_PROFILES_ACTIVE=local
# 2) Перевизначимо конкретну властивість застосунку (директорію сховища)
export APP_ATTACHMENTS_STORAGE_DIR=./runtime/attachments
# 3) Перевизначимо властивість фреймворку (ліміт розміру файлу)
export SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE=30MB
На Windows у PowerShell синтаксис інший, але ідея та сама: змінна середовища — це «останнє слово», яке ви кажете конфігурації перед запуском застосунку.
Чому це зручно саме в нашому Task Tracker API? Бо директорії — річ дуже особиста. Один розробник хоче зберігати файли в ./tmp, інший — у C:\work\data, третій — у ~/dev/task-tracker-attachments. Якщо ви спробуєте розв’язати це профілями й комітити різні шляхи, ви отримаєте «конфігураційну війну». Якщо ви розв’яжете це через змінні середовища — усі задоволені, а репозиторій залишається чистим.
Пріоритети конфігурації та налагодження
Коли ви починаєте використовувати і профілі, і env vars, і значення за замовчуванням у @ConfigurationProperties, виникає закономірне запитання: «якщо одна й та сама властивість задана в трьох місцях — яке значення візьметься?». Це запитання не філософське, а цілком практичне: без розуміння пріоритетів ви витрачатимете години на «я точно змінив, чому не працює».
У навчальній версії можна тримати просту ментальну модель: конфігурація — це шари, і чим ближче до запуску, тим вищий пріоритет. Базовий application.yml дає значення за замовчуванням. Профільний файл підміняє частину значень. Змінні середовища накладаються поверх профілю. Параметри командного рядка зазвичай ще вищі й можуть перевизначити навіть змінні середовища.
| Джерело значення | Приклад | Пріоритет (хто «перекрикує» кого) |
|---|---|---|
| Значення за замовчуванням у коді | private String storageDir = "./data/attachments"; | найнижчий |
| application.yml (базовий) | |
вище за значення за замовчуванням |
| application-local.yml | |
вище за базовий |
| Змінні середовища | |
вище за файли |
| Аргументи командного рядка | |
зазвичай найвищий |
Якщо ви ловите ситуацію «я змінив YAML, а значення не змінюється», майже завжди причина одна з двох: або ви запускаєте не той профіль, або значення перевизначене змінною середовища чи аргументом. Тож, коли щось поводиться дивно, не треба починати з містики. Почніть із діагностики: перевірте активний профіль, а потім — чи немає у вас змінної середовища, яка перевизначає ту саму властивість.
Для закріплення можна уявити це як дуже просту блок-схему: Spring Boot іде зверху вниз і «збирає» конфіг, але при конфлікті перемагає джерело з вищим пріоритетом.
flowchart TD
%% Ідея: нижче — джерела з вищим пріоритетом, вони "перекривають" верхні
A["Значення за замовчуванням у коді"] --> B["application.yml"]
B --> C["application-local.yml"]
C --> D["Змінні середовища"]
D --> E["Аргументи командного рядка"]
%% Підсумкове значення, яке побачить ваш код у @ConfigurationProperties
E --> F["Підсумкове значення в @ConfigurationProperties"]
Наприкінці цієї теми корисно вміти відповідати собі на кілька запитань без підказок: чи можете ви пояснити, чому storage-dir має саме таке значення; чи розумієте ви, як увімкнути local профіль в одному конкретному запуску; чи можете ви перевизначити один параметр через змінну середовища, не чіпаючи YAML; чи можете ви швидко довести собі, що профіль справді активний, а не «здається, активний».
4. Типові помилки під час роботи з профілями та перевизначеннями
Помилка №1: копіювати в профіль увесь базовий конфіг цілком.
Так зазвичай трапляється «за звичкою»: розробник створює application-local.yml, бере весь application.yml, вставляє його туди й змінює один рядок. Через тиждень базова конфігурація змінюється, а профільна — ні. Виходять два світи, які розходяться, і налагодження перетворюється на ворожіння. Профіль має містити лише відмінності, інакше він перестає бути перевизначенням і стає «другою реальністю».
Помилка №2: змушувати профіль змінювати сенс API-контракту.
Дуже хочеться зробити «локально статус може бути будь-який, а на бойовому середовищі — ні», або «локально вимкнемо validation, щоб не заважала», або «локально віддаємо помилки як рядок, а на продакшені — ProblemDetail». Це руйнує саму ідею контракту: клієнт не має залежати від того, який профіль сервера ввімкнено сьогодні. Профіль — про середовище запуску, а не про правила домену, не про дозволені переходи статусів і не про форму DTO. Якщо значення впливає на контракт, йому місце в коді та в документації, а не в application-local.yml.
Помилка №3: забувати, який профіль активний, і перевіряти це методом «поскаржуся на життя».
Типова ситуація: ви впевнені, що ввімкнули local, але насправді застосунок стартував без профілю, і тому storage-dir узявся з базової конфігурації. Або навпаки: профіль увімкнено десь в IDE, ви забули про це, і потім дивуєтеся, чому на ноутбуці все працює «не так, як на сервері». Лікується це просто: у розробці завжди виводьте активний профіль, а в IDE тримайте окремі Run Configurations «Default» і «Local», щоб не перемикати їх туди-сюди вручну.
Помилка №4: класти залежний від машини абсолютний шлях у базовий application.yml.
Базова конфігурація — це те, що мають однаково читати всі. Якщо ви пишете туди /Users/alex/... або D:\attachments, ви перетворюєте репозиторій на «конфіг для однієї машини». Для базової конфігурації краще відносний шлях на кшталт ./data/attachments. Якщо вже комусь потрібен абсолютний шлях — нехай задає його через змінну середовища або через локальний профіль, який не комітиться.
Помилка №5: не розуміти, що змінні середовища можуть «перекричати» YAML, і тому не вірити своїм очам.
Сценарій виглядає кумедно: ви змінюєте application.yml, перезапускаєте, а значення не змінюється. Починаються підозри в змові Spring Boot. Насправді найчастіше у вас десь уже встановлено APP_ATTACHMENTS_STORAGE_DIR, і воно перемогло. У цей момент корисно не сваритися на Spring, а спокійно подивитися на змінні середовища й пам’ятати порядок пріоритетів. Spring Boot тут якраз поводиться дуже послідовно — це ми просто забули, що самі ж залишили перевизначення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ