JavaRush /Курси /Spring Boot /Запуск jar: профілі та конфіги

Запуск jar: профілі та конфіги

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

1. Конфігурація після збирання jar

Коли застосунок живе в IDE, мозок швидко звикає до ілюзії, ніби «проєкт = застосунок». Усередині IDE поруч лежить папка src/main/resources, профілі вмикаються галочкою в Run Configuration, а потрібні параметри можуть випадково зберегтися в налаштуваннях запуску. Але jar-файл — це вже «валіза з речами»: ви її віддали — і все, далі у вас немає права непомітно підкласти туди ще один носок.

І тут раптово зʼясовується, що конфігурація — це не декоративний YAML «для краси», а частина контракту запуску. Для сервісу на Spring Boot нормальна модель така: один і той самий jar запускається в різних середовищах (local/dev/prod), а відмінності задаються профілями, змінними середовища, аргументами командного рядка та зовнішніми конфігами. Якщо ви це вмієте — ви справді вмієте Spring Boot. Якщо ні — ви вмієте натискати зелений трикутник (і це теж навичка, просто… трохи менш монетизована).

У цій лекції будемо вважати, що jar уже зібрано і він лежить, наприклад, тут:

build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Якщо у вас назва інша — це нормально: головне, щоб ви запускали той файл, який ви щойно зібрали.

2. Звідки Spring Boot бере властивості під час запуску jar

Перш ніж перевизначати профілі та властивості, корисно навести лад у голові: Boot спочатку збирає набір джерел властивостей (property sources), а потім застосовує правило пріоритету. Ми цю модель уже обговорювали в модулі про externalized configuration, але зараз вона стає особливо практичною: ви буквально «крутите ручки», а застосунок або слухається, або ні.

Нижче — спрощена карта джерел, без спроби перетворити лекцію на довідник. Важливо зрозуміти принцип: у цій схемі пріоритет зростає зверху вниз. Те, що стоїть нижче, сильніше перекриває те, що було задано вище. SPRING_APPLICATION_JSON поки не включаємо в діаграму, бо окремо розберемо його трохи нижче.

flowchart TB
  A["Низький пріоритет: всередині jar — application.yaml і application-{profile}.yaml"] --> B["Зовнішні файли конфігурації: ./config, додаткові location/import"]
  B --> C["Змінні середовища"]
  C --> D["Системні властивості Java -D..."]
  D --> E["Високий пріоритет: аргументи командного рядка --key=value"]
  E --> F["Підсумкова конфігурація в Environment"]

Найчастіша несподіванка під час запуску з артефакта: людина змінює application.yaml у проєкті й очікує, що це вплине на вже зібраний jar. Але jar — це «фото на паспорт»: воно не змінюється від того, що ви постриглися. Якщо ви правите ресурси — jar треба повторно зібрати. Якщо хочете змінювати поведінку без повторного збирання — використовуйте перевизначення під час запуску (env vars, -D, --..., зовнішні конфіги).

Для catalog-service (за ТЗ курсу) базові конфігурації зазвичай такі:

application.yaml — загальний.
application-local.yaml, application-dev.yaml, application-prod.yaml — профільні.
catalog-data.yaml — імпортований файл зі списком курсів (через spring.config.import).

І все це може жити всередині jar, а поверх — у вас можуть бути зовнішні файли (наприклад, ./config/catalog-extra.yaml) або взагалі окрема папка конфігурації, що лежить поруч із jar на сервері.

3. Увімкнення профілю під час java -jar: CLI args, env vars і -D

Профіль — це не «режим роботи застосунку для краси», а спосіб увімкнути потрібну конфігурацію і (іноді) потрібні біни. У нашому курсі профілі — один із ключових інструментів: local для зручної розробки, dev для більш «реалістичної» діагностики, prod для мінімально безпечного рівня відкритості та суворіших налаштувань.

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

Найпряміший і найнаочніший спосіб — аргумент командного рядка:

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=dev

Якщо ви любите змінні середовища (а вони зазвичай чудово вписуються в запуск сервісів), то це буде так:

SPRING_PROFILES_ACTIVE=prod \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Третій варіант — Java system properties через -D. Це трохи «олдскульніше», але й досі робочий і поширений шлях:

java -Dspring.profiles.active=local \
  -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Що обрати? У навчальному проєкті — будь-що, аби ви могли відтворити запуск без IDE. У реальному житті часто перемагають env vars (вони добре дружать із процесними менеджерами) або CLI args (вони максимально явні). Головне — не тримати профіль «тільки в IDE», бо тоді запуск із jar перетворюється на лотерею.

4. Перевизначаємо прикладні властивості під час запуску jar: --, env vars і -D

Профіль — це лише початок. Справжня сила externalized configuration — у тому, що ви можете змінювати поведінку застосунку точково: порт, прапорці поведінки, ліміти, діагностичні налаштування. І все це — без повторного збирання jar, просто завдяки параметрам запуску.

CLI args: перевизначаємо прямо під час запуску

CLI args у Boot виглядають як --key=value. Для запуску з jar це особливо зручно, бо у вас завжди є один рядок, який можна скопіювати, зберегти в README або передати колезі.

Наприклад, запустимо catalog-service у профілі dev і на іншому порту:

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=dev \
  --server.port=9090

Тепер додамо перевизначення прикладної властивості. За ТЗ у нас є namespace app.catalog.*, і, припустімо, ми хочемо збільшити ліміт обраних курсів:

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=dev \
  --app.catalog.max-featured-count=6

Якщо у вашому коді є поведінка, зав’язана на maintenance-mode, можна увімкнути її так само:

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=local \
  --app.catalog.maintenance-mode=true

Зверніть увагу на маленьку, але важливу річ: ми передаємо властивості у канонічній формі, тій самій, що в YAML. Boot далі сам розбереться з naming conventions і binding.

Env vars: перевизначаємо через середовище

Змінні середовища — класний спосіб, коли рядок запуску не повинен роздуватися або коли ви запускаєте сервіс через якийсь менеджер, який зручно підставляє env vars.

Для server.port це виглядає так:

SPRING_PROFILES_ACTIVE=dev \
SERVER_PORT=9090 \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

А ось для app.catalog.max-featured-count важливо одразу тримати одну канонічну форму, щоб не плодити конкуруючі варіанти найменування. В env vars Boot спирається на canonical property name: крапки перетворюються на _, дефіси з kebab-case прибираються, усе переводиться у верхній регістр. Тому app.catalog.max-featured-count перетворюється на APP_CATALOG_MAXFEATUREDCOUNT.

SPRING_PROFILES_ACTIVE=dev \
APP_CATALOG_MAXFEATUREDCOUNT=6 \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Невелика таблиця-шпаргалка (саме для властивостей нашого проєкту):

Канонічна властивість (YAML/CLI) Приклад env var
spring.profiles.active SPRING_PROFILES_ACTIVE
server.port SERVER_PORT
app.catalog.title APP_CATALOG_TITLE
app.catalog.max-featured-count APP_CATALOG_MAXFEATUREDCOUNT
app.catalog.maintenance-mode APP_CATALOG_MAINTENANCEMODE

System properties -D: перевизначаємо в JVM

System properties корисні, коли ви керуєте запуском JVM через один «JVM-рядок». Наприклад:

# У цьому варіанті всі перевизначення передаються як властивості JVM
java \
  -Dspring.profiles.active=prod \
  -Dserver.port=8085 \
  -Dapp.catalog.max-featured-count=4 \
  -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Це не краще й не гірше, ніж env vars або CLI args — просто інший канал доставки. Але пам’ятати про нього корисно: ви досить часто бачитимете -D... у реальних проєктах.

5. SPRING_APPLICATION_JSON

Іноді хочеться передати не одну властивість, а одразу набір, і робити для цього десять env vars — сумно. У такі моменти Spring Boot дає механізм SPRING_APPLICATION_JSON: ви передаєте JSON-рядок, усередині якого лежить «шматок конфігурації», і Boot сприймає це як ще одне property source.

Це схоже на ситуацію «ось вам міні-application.yaml, тільки у форматі JSON». Приклад (зверніть увагу: JSON-лапки — ваше нове джерело пригод, особливо в різних оболонках):

# Одна env var, усередині якої «шматок конфігурації»
# Важливо: лапки/екранування залежать від вашої оболонки (bash/zsh/powershell тощо)
SPRING_APPLICATION_JSON='{
  "spring": { "profiles": { "active": "dev" } },
  "server": { "port": 9090 },
  "app": { "catalog": { "max-featured-count": 6 } }
}' \
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar

Сенс тут простий: ми однією змінною середовища ввімкнули профіль, змінили порт і перевизначили ліміт обраних курсів.

Чому цей механізм корисний саме в контексті packaged-run? Тому що ви починаєте мислити як інженер, який «перевозить» застосунок між середовищами: jar один, налаштування змінюються зовні. SPRING_APPLICATION_JSON — один зі способів покласти частину налаштувань «поруч із запуском», не створюючи окремий файл.

І так, якщо ви зараз подумали «звучить класно, але я точно десь забуду лапку», — це абсолютно нормальне відчуття. Навіть досвідчені розробники іноді ловлять такі помилки, бо JSON у змінній середовища — це як зібрати IKEA без інструкції, але з упевненістю в собі.

6. Зовнішні конфіги: additional-location і location

До цього моменту ми змінювали поведінку через параметри запуску (env vars, -D, CLI args). Але часто є ще більш «операторський» сценарій: конфіг має жити в окремому файлі, поруч із jar або в окремій директорії, щоб його можна було версіонувати, змінювати, підкладати на різні машини, не змінюючи процес збирання.

У нашому проєкті за ТЗ навіть передбачена папка ./config/ і опційний зовнішній файл ./config/catalog-extra.yaml. Тепер питання: як сказати Boot «подивися ще й туди»?

spring.config.additional-location: акуратно розширюємо стандартний пошук

spring.config.additional-location додає ще одну локацію до стандартного пошуку. Це означає: Boot як завжди прочитає конфіги з jar і стандартних зовнішніх місць, а потім додатково зазирне в указану локацію.

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.profiles.active=dev \
  --spring.config.additional-location=optional:file:./config/

Тут важливі три деталі.

По-перше, file: — це явний вказівник, що ми читаємо з файлової системи, а не з classpath.

По-друге, ./config/ — відносний шлях. Він буде розвʼязуватися відносно поточної директорії процесу, а не відносно розташування jar. Це ми ще окремо розберемо нижче, бо це одна з найчастіших причин «чому воно не працює?!».

По-третє, optional: означає «якщо папки/файлу немає — не падай». Це корисно, коли зовнішній конфіг не є обов’язковим. У навчальному проєкті це часто зручно: можна запускати і без ./config, і з нею.

Як Boot шукатиме файли в цій директорії? Зазвичай він шукає стандартні імена на кшталт application.yaml, application-dev.yaml тощо. Тобто якщо ви покладете в ./config/ файл application-dev.yaml, він може перевизначити те, що лежить усередині jar, не вимагаючи повторного збирання.

spring.config.location: повністю замінюємо стандартний пошук

spring.config.location — це інший інструмент. Він не «додає ще одне місце», а замінює стандартний пошук конфігурації на те, що ви вказали. Це сильна й іноді небезпечна дія: можна випадково «відключити» завантаження внутрішніх ресурсів, до яких ви звикли.

java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.config.location=file:./config/

Коли це доречно? Коли ви хочете жорстко сказати: «Запуск без зовнішнього конфіга заборонено». Так буває в реальних системах, де конфіг має приїжджати окремо (наприклад, разом із секретами), і запуск «на дефолтах» вважається помилкою.

Але в навчальному проєкті та в більшості перших сервісів я б починав саме з additional-location, бо він м’якший і простіший для дебагу: ви додали зовнішній файл — побачили ефект; прибрали — повернулися до конфіга всередині jar.

7. Перевіряємо, що профіль і перевизначення застосувалися

Коли конфігурація задається «ззовні», завжди є ризик самообману: ви впевнені, що передали потрібний параметр, а застосунок продовжує жити по-старому. І тут важливо не гадати, а перевіряти. На щастя, ми вже побудували для catalog-service хороший діагностичний базис: логи та Actuator.

Найпростіший і «бідний, але чесний» спосіб — логувати важливі параметри під час старту. У нашому проєкті для цього і існує StartupSummaryRunner: він має повідомляти, з якими профілями і ключовими налаштуваннями стартував сервіс. Це особливо важливо в режимі запуску з jar, бо там IDE більше не підсвічує вам активний профіль красивою галочкою.

Невеликий приклад того, як може виглядати фрагмент журналу старту (ідея, а не єдиний правильний формат):

Активні профілі: dev
Порт сервера: 9090
Заголовок каталогу: Spring+ Catalog
Максимальна кількість featured-курсів: 6

Якщо ви хочете зробити цей лог надійнішим і прив’язати його до нашої типізованої конфігурації, ось компактний приклад (не «повна версія класу», а шматок, який демонструє ідею). Зверніть увагу на імпорти: я спеціально пишу їх явно, щоб ви бачили, звідки беруться типи.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component // Реєструємо клас як bean, щоб він створився під час старту контексту
public class StartupConfigLog {

    private static final Logger log = LoggerFactory.getLogger(StartupConfigLog.class);

    public StartupConfigLog(CatalogProperties props, Environment env) {
        // Environment — «фінальний» шар, де видно всі джерела властивостей з урахуванням пріоритетів
        log.info("Активні профілі: {}", String.join(",", env.getActiveProfiles()));

        // Типізована конфігурація (CatalogProperties) показує, що binding справді спрацював
        log.info("Максимальна кількість featured-курсів: {}", props.maxFeaturedCount()); // наприклад: 6
    }
}

Так, цей приклад трохи «шахрайський»: він логує в конструкторі. У реальному коді ви б робили це через ApplicationRunner, щоб логувати вже після підняття контексту в передбачуваній точці життєвого циклу (це ми робили раніше). Але як ілюстрація зв’язку Environment + CatalogProperties + лог він добре працює й займає рівно стільки рядків, скільки потрібно, щоб не потонути.

Другий спосіб — Actuator. Якщо в профілі local/dev у вас відкрито /actuator/env і /actuator/configprops, ви можете перевірити фактичне значення прямо з процесу, що працює. Наприклад (умовний запит, формат залежить від того, як ви відкриваєте endpoint — через браузер, curl або HTTP-файл):

GET http://localhost:9090/actuator/env
GET http://localhost:9090/actuator/configprops

Сенс цих перевірок простий: якщо ви передали --app.catalog.max-featured-count=6, то маєте побачити, що в Environment справді фінальне значення дорівнює 6, і що CatalogProperties теж отримує це значення.

8. Відносні шляхи та робоча директорія процесу

Є одна підступна штука, яка ламає запуск зовнішніх конфігів частіше, ніж Spring, Gradle та космічна радіація разом узяті. Це поточна директорія процесу (working directory). Коли ви пишете file:./config/, ви насправді кажете: «візьми папку config відносно того місця, звідки я запустив команду».

А місце, звідки ви запускаєте команду, може бути різним. Наприклад, ви можете:

- перебувати в корені проєкту та запускати java -jar build/libs/...;
- перебувати всередині build/libs і запускати java -jar catalog-service-...;
- запускати jar взагалі з іншої папки, куди ви його скопіювали.

І у всіх трьох випадках ./config/ — це різні шляхи.

Приклад, який виглядає однаково «за змістом», але відрізняється working directory:

# Варіант 1: запускаємо з кореня проєкту
java -jar build/libs/catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.config.additional-location=optional:file:./config/
# Варіант 2: спочатку зайшли в build/libs, і тепер ./config дивиться вже туди
cd build/libs
java -jar catalog-service-0.0.1-SNAPSHOT.jar \
  --spring.config.additional-location=optional:file:./config/

У другому варіанті Boot шукатиме build/libs/config, а не <project-root>/config. Якщо ви про це не думали — ви будете впевнені, що «Spring Boot зламався». А він просто чесно робить те, що ви написали.

Як це виправити? Найпростіший підхід — запускати процес зі «стабільної» директорії (там, де ви очікуєте конфіг), або задавати шлях явно (наприклад, абсолютний), або тримати зовнішній конфіг поруч із jar і запускати з цієї ж папки.

9. Типові помилки під час запуску jar

Помилка № 1: запускати jar без профілю й дивуватися, чому “у dev у мене було інакше”.
Коли ви запускали застосунок з IDE, профіль міг бути увімкнений налаштуванням IDE або змінною середовища, яку IDE підставляє. У jar-запуску цього «сервісу з читання ваших думок» уже немає. Тому ви запускаєте без профілю й отримуєте поведінку за замовчуванням. Лікується просто: профіль має бути частиною команди запуску або середовища, а не частиною пам’яті IDE.

Помилка № 2: змінювати application.yaml у проєкті й не пересбирати jar.
Це класика жанру. Ви правите ресурси, запускаєте старий jar і думаєте, що зміни «не працюють». Але jar — це окремий файл, він не дивиться на вашу папку src. Якщо ви хочете, щоб зміни всередині ресурсів потрапили в артефакт, потрібно зібрати новий jar. Якщо ви хочете змінювати поведінку без збирання — використовуйте перевизначення під час запуску або зовнішній конфіг.

Помилка № 3: плутати spring.config.additional-location і spring.config.location.
additional-location розширює пошук і зазвичай «прощає» відсутність зовнішніх файлів, особливо з optional:. location замінює стандартний пошук і може раптово вимкнути частину звичної конфігураційної поведінки. Якщо ви не впевнені, що вам потрібна жорстка заборона запуску без зовнішнього конфіга, починайте з additional-location.

Помилка № 4: неправильно назвати env var для прикладної властивості.
Коли ви переходите від --app.catalog.max-featured-count=6 до env vars, легко помилитися в назві й отримати відчуття «змінна є, а властивість не застосувалася». Допомагає дисципліна: тримайтеся однієї канонічної форми на кшталт APP_CATALOG_MAXFEATUREDCOUNT, а потім перевіряйте фактичне значення через startup-логи або Actuator (env/configprops).

Помилка № 5: не враховувати поточну директорію процесу й губити зовнішній конфіг.
file:./config/ — це не «поруч із jar», а «поруч із working directory». Якщо ви запускаєте jar з іншої папки, відносний шлях змінює сенс. Це не баг Spring Boot і не привід писати “ну тоді я захардкоджу шлях у коді” (будь ласка, не треба). Це привід зробити запуск відтворюваним і усвідомленим: або фіксувати директорію запуску, або вказувати шлях явно.

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