JavaRush /Курси /Docker for Spring /Зовнішні конфіги: import

Зовнішні конфіги: import і location

Docker for Spring
Рівень 12 , Лекція 3
Відкрита

1. Коли стандартного пошуку недостатньо

Якщо ви вперше чуєте про spring.config.import і його родичів, може здатися, що починається легка паніка: «То що, раніше все було неправильно?» Не хвилюйтеся. У більшості випадків Spring Boot справді сам знаходить application.yml і application-{profile}.yml у очікуваних місцях. Це хороша базова магія: вона економить час і нерви. Але іноді вам потрібна не магія, а керований сценарій.

Поки зовнішній файл називався application.yml або application-postgres.yml і лежав у звичному місці поруч із запуском, Spring Boot часто міг знайти його сам. Але щойно ім’я стає довільним — наприклад runtime.yml — або шлях перестає бути стандартним, автопошуку вже недостатньо. Тут і з’являються spring.config.import, spring.config.location і spring.config.additional-location: вони потрібні не замість звичних application*.yml, а для явного керування нестандартними файлами та маршрутами пошуку.

Уявіть дві типові ситуації для навчального сервісу. В одній ви хочете тримати всередині jar зрозумілу базову конфігурацію, яка завжди запускається, а поруч мати невеликий файл runtime.yml, який може бути, а може й не бути. Якщо його немає, застосунок не повинен падати. В іншій ви хочете сказати: «Ось тут лежать мої конфіги, і тільки тут. Не шукайте їх по всіх кишенях куртки й у кожному config/». Саме для таких сценаріїв і існують три інструменти: spring.config.import, spring.config.location і spring.config.additional-location.

Щоб не загубитися, триматимемо в голові просту метафору. За замовчуванням Spring Boot поводиться як людина, яка шукає ключі від квартири за звичним маршрутом: «у кишені, на столі, в рюкзаку». additional-location — це коли ви додаєте до маршруту ще одне місце: «і ще в шухляді в передпокої». location — це коли ви кажете: «стоп, шукаємо тільки в шухляді, більше ніде». А import — це коли ви відкрили блокнот «де я зберігаю ключі» і всередині блокнота написали: «а ще перевір конверт у папці».

2. spring.config.import: підключаємо файл

spring.config.import найчастіше сприймається як ще один спосіб підключити конфіг, але корисніше думати про нього як про прикріплення одного конфігураційного документа до іншого. Ви берете основний application.yml (або профільний файл) і кажете: «Якщо існує такий зовнішній файл — дочитай його теж». У контейнерному світі це зручно, тому що ви не перескладаєте образ, а лише додаєте застосунку ще один шар налаштувань.

Де оголошувати import

Дуже важливо не заплутатися в тому, що і де пишеться. spring.config.import можна оголошувати всередині конфігураційного файла, тобто прямо в application.yml (який у нас лежить у src/main/resources і потрапляє всередину jar). Тоді виходить приємний ефект: образ і jar завжди містять базову конфігурацію, а зовнішній файл стає опційною надбудовою.

Уявімо, що в нашому сервісі вже є властивість app.export-dir (вона нам потрібна для експорту каталогу в CSV). Усередині jar ми хочемо мати безпечне значення за замовчуванням, наприклад /app/exports, а на конкретній машині розробника хочемо перевизначити його на щось на кшталт /workspace/catalog-exports.

Тоді базовий application.yml може виглядати так:

# src/main/resources/application.yml
spring:
  config:
    import: "optional:file:/config/runtime.yml" # Опціонально підключаємо зовнішній шар (якщо файла немає — стартуємо далі)

app:
  export-dir: /app/exports # Базове значення всередині jar; безпечне значення за замовчуванням

Зверніть увагу на дві речі. По-перше, ми не робимо зовнішній файл обов’язковим. По-друге, ми явно використовуємо file:/... і фіксований шлях усередині контейнера. Йдеться не про файлову механіку контейнера, а про те, що застосунок очікує побачити файл за конкретним шляхом.

Тут /config/runtime.yml — уже не той звичний зовнішній application.yml, який Boot упізнає за ім’ям. Ми спеціально обрали довільне ім’я та фіксований шлях, тож файл підключається явно.

А зовнішній файл (який, наприклад, може бути поруч із запуском або опинитися в контейнері за вказаним шляхом) буде дуже коротким:

# /config/runtime.yml (зовнішній файл)
app:
  export-dir: /workspace/catalog-exports # Перевизначення під конкретну машину/запуск
server:
  port: 9090 # Приклад: локально хочемо слухати інший порт

Ідея в тому, що зовнішній файл зберігає лише те, що реально стосується цієї машини або цього запуску, а не копіює весь конфіг цілком.

optional: і запуск без файла

З префіксом optional: усе просто, але наслідки великі. Без optional: Spring Boot вважатиме, що імпортований ресурс обов’язково має існувати, і якщо його немає, старт може завершитися помилкою. Для навчального проєкту це майже завжди незручно: ви хочете, щоб базова конфігурація запускалася з коробки, без того, щоб студент спочатку створював секретні файли в секретних місцях.

Тому в нашому курсі добра дисципліна така: якщо імпортований файл — це опційний шар, то в import майже завжди буде optional:.

spring:
  config:
    import: "optional:file:/config/runtime.yml"

Сенс тут дуже людяний: «Спробуй дочитати, але якщо не вийде — не переймайся і запускайся на тому, що є».

Fixed і relative шляхи в import

Тепер тонкий момент, через який найчастіше й з’являються конфігураційні «привиди». У spring.config.import є два поширені стилі шляху: fixed location і relative (відносний до файла, який оголосив import). У плані заняття ми називаємо це «fixed location» і «import-relative location».

Fixed location — це коли ви явно вказуєте, де лежить файл, і шлях не залежить від того, де знаходиться сам конфіг, який імпортує. Наприклад:

spring:
  config:
    import: "optional:file:/config/runtime.yml"

Це хороший варіант для контейнера: ви заздалегідь домовилися, що конфіги (якщо вони взагалі будуть зовнішніми) лежать, скажімо, у /config.

Relative import — це коли ви пишете шлях без явного file:/classpath: і без абсолютного /..., і тоді Spring Boot сприймає його як «файл поруч» (відносно поточного конфігураційного документа). Наприклад:

spring:
  config:
    import: "optional:extra/runtime.yml"

Навіщо це потрібно? Для підходу з окремою папкою. Уявіть, що ви зберігаєте пакет зовнішніх конфігів в одному каталозі, наприклад:

config/
├── application.yml
└── extra/
    └── runtime.yml

Тоді вам зручно всередині config/application.yml написати відносний імпорт — і вся структура стає переносною цілком: скопіювали папку config/ на іншу машину, і шляхи не розсипалися.

Важливо не намагатися вгадати «як саме Boot обчислює цей шлях» на відчуттях. Найкраща звичка для новачка — завжди ставити собі запитання: «Я хочу, щоб шлях був прив’язаний до конкретного місця (fixed), чи хочу, щоб він був прив’язаний до поточного конфига (relative)?» Якщо ви самі собі на це відповісте, 80% плутанини зникне.

Мінідемо: перевіряємо перевизначення

Щоб не перетворювати тему на теорію про YAML, корисно мати в проєкті маленький «датчик», який показує підсумкове значення. Найпростіший датчик — залогувати app.export-dir на старті.

Припустімо, у нас є конфігураційні властивості:

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "app") // Пов'язуємо властивості на кшталт app.* із цим типом
public record AppProperties(String exportDir) { } // exportDir <- app.export-dir (relaxed binding)

Припустімо, що AppProperties уже зареєстрований як @ConfigurationProperties-тип для app.*; тут він потрібен лише як читабельний контейнер підсумкових значень.

І невеликий лог на старті:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
class StartupLoggingConfig {
  private static final Logger log = LoggerFactory.getLogger(StartupLoggingConfig.class);

  @Bean
  ApplicationRunner logExportDir(AppProperties props) { // Bean буде виконано під час запуску застосунку
    return args -> log.info("app.export-dir = {}", props.exportDir()); // {} — плейсхолдер SLF4J, значення беремо з підсумкової конфігурації
  }
}

Якщо такого налаштування в проєкті немає, додайте звичайне сканування або реєстрацію @ConfigurationProperties (@ConfigurationPropertiesScan або @EnableConfigurationProperties).

Далі ви вже подумки зіставляєте: якщо зовнішній runtime.yml існує та його імпортовано, у логах ви побачите /workspace/catalog-exports. Якщо ні — побачите значення за замовчуванням /app/exports. Жодної містики, просто перевіряємо, що реально вийшло.

3. spring.config.location: тільки вказані шляхи

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

Як location замінює дефолти

Під стандартними місцями пошуку ми зараз розуміємо той самий набір, де Boot зазвичай шукає application.yml і application-{profile}.yml: усередині jar (classpath) і поруч із запуском (current dir, config/ тощо). Коли ви використовуєте spring.config.location, ви ніби кажете: «Не шукай конфіг за стандартними правилами. Я сам указую, де він лежить».

Звідси головний практичний висновок. spring.config.location — штука сильна, але небезпечна для новачка. Вона корисна, коли ви точно хочете контролювати оточення і розумієте, що робите. Але вона може призвести до ситуації: «Чому застосунок раптом перестав бачити application.yml, який я поклав у src/main/resources?» Відповідь неприємно проста: тому що ви самі попросили Boot туди не дивитися.

У навчальному проєкті такі постріли собі в ногу особливо прикрі, тому що ви втрачаєте базовий запуск. Тому в межах курсу ми ставимося до spring.config.location як до інструмента «по ділу», а не як до налаштування «про всяк випадок».

Як задавати location зовні

Тепер важлива механіка, яку легко зрозуміти на побутовому рівні. Щоб Spring Boot знайшов конфігурацію, йому спочатку потрібно зрозуміти, де її шукати. А отже, параметри «де шукати» мають прийти раніше, ніж самі конфіги. Тому spring.config.locationspring.config.additional-location) задають як:

- env var (наприклад, SPRING_CONFIG_LOCATION),
- JVM system property (-Dspring.config.location=...),
- command-line argument (--spring.config.location=...).

Намагатися прописати spring.config.location всередині application.yml, який Boot ще не знайшов, — це логічна петля: «Візьми карту зі шухляди, яку ти знайдеш за картою».

Приклад із -D виглядає так:

# JVM system property: задаємо шлях пошуку ще до читання application.yml
java -Dspring.config.location=optional:file:/config/ -jar app.jar

А у вигляді аргументу застосунку — так:

# Аргумент застосунку: ефект той самий, але читається як параметр запуску
java -jar app.jar --spring.config.location=optional:file:/config/

У контейнері те саме, тільки часто зручніше через env vars (згадуємо relaxed binding із минулого дня):

# Змінна середовища: часто найзручніший шлях для контейнерів
SPRING_CONFIG_LOCATION=optional:file:/config/

Ключовий момент тут не в синтаксисі, а в інженерній ідеї: ці властивості читаються дуже рано, тому вони мають бути доступні ще до того, як Boot почне гортати ваші application-файли.

Коли location доречний

Щоб не демонізувати інструмент, чесно окреслимо сценарій, де він доречний. Уявіть, що ви запускаєте один і той самий jar в оточенні, де взагалі не хочете використовувати внутрішні дефолти (наприклад, тому що маєте жорстко задану конфігурацію і базові значення в jar вам не потрібні). Тоді location дає змогу сказати: «Моя конфігурація — тільки в цьому каталозі».

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

4. spring.config.additional-location: додаємо шлях пошуку

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

Різниця між location і additional-location

Замість довгих абстракцій скажімо так. Якщо ви використовуєте additional-location, то внутрішній application.yml (із src/main/resources) продовжує існувати як «скелет» конфігурації, а зовнішній каталог стає «додатковою папкою, де теж можуть лежати application-файли». Це дуже схоже на ідею не копіювати все назовні, а винести лише те, що потрібно.

Наприклад, у вас всередині jar є:

- application.yml із загальними налаштуваннями,
- application-postgres.yml тощо.

А зовні ви хочете перевизначити тільки server.port і app.export-dir під конкретну машину. Тоді ви кладете зовнішній application.yml (короткий) у додатковий каталог і кажете Boot: «Подивись ще й сюди».

Задається це знову ж таки рано, «ззовні»:

java -jar app.jar --spring.config.additional-location=optional:file:/config/

або так:

java -Dspring.config.additional-location=optional:file:/config/ -jar app.jar

або через env vars:

SPRING_CONFIG_ADDITIONAL_LOCATION=optional:file:/config/

Приклад структури зовнішніх файлів

Давайте уявимо охайну структуру зовнішнього каталогу. Не десять файлів «final-prod-really.yml», а дуже нудно й зрозуміло:

/config/
├── application.yml
└── application-postgres.yml

Тоді зовнішній application.yml може бути таким:

# /config/application.yml (зовнішній)
server:
  port: 9090

app:
  export-dir: /workspace/catalog-exports

А якщо ви хочете винести назовні саме postgres-параметри (наприклад, тому що на різних машинах різні облікові дані або різні хости), ви можете мати зовнішній application-postgres.yml:

# /config/application-postgres.yml (зовнішній)
spring:
  datasource:
    url: jdbc:postgresql://postgres:5432/catalog
    username: catalog
    password: catalog

Сенс у тому, що Boot працюватиме з цими файлами так само, як і з внутрішніми: профіль активований — профільний файл бере участь; профіль не активований — не бере участі. І все це без перескладання образу та без копіювання Dockerfile. Ми лишаємося в парадигмі «один image, різні конфіги».

optional: для additional-location

Так само, як і з import, ви часто хочете, щоб каталог був «якщо є — використовуй, якщо немає — не ламай запуск». Тому і тут зручно використовувати optional:.

--spring.config.additional-location=optional:file:/config/

Тоді студент може запустити сервіс узагалі без зовнішнього каталогу, і все працюватиме на базовій конфігурації всередині jar. А коли зовнішній каталог з’явиться — як усвідомлений інструмент, а не обов’язкова точка входу — він почне впливати на конфігурацію.

5. Шпаргалка вибору механізму

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

Щоб спростити вибір, ось таблиця, яку можна тримати в голові — і не соромитися повертатися до неї:

Механізм Що ви кажете Boot Коли зазвичай підходить Що легко зламати
spring.config.import «Із цього конфига дочитай ще ось цей файл» Коли потрібен один конкретний додатковий файл (часто runtime.yml) і ви хочете керувати цим прямо з application.yml Якщо забути optional: і файла немає — старт може впасти
spring.config .additional-location «Шукай application-файли ще й у цьому каталозі, але стандартні правила залиш» Коли потрібен зовнішній application.yml/application-{profile}.yml як шар поверх бази всередині jar Якщо ви очікуєте, що це «заміна», а це «додаток», можна здивуватися підсумковому precedence
spring.config .location «Шукай конфіг тільки за цим маршрутом (по суті — перепиши маршрут пошуку)» Коли ви свідомо хочете вимкнути default locations і жити тільки на зовнішньому конфігу Дуже легко вимкнути classpath-конфіг і втратити базу

Якщо коротко, у стилі «як би я пояснив другу»: import — це про «підключити один файл до іншого», additional-location — про «додати ще одну папку пошуку», location — про «замінити весь список папок пошуку».

6. Типові помилки

Помилка № 1: використовувати spring.config.location, коли насправді треба було просто додати ще один конфіг.
Це класика. Людина хоче «підсунути зовнішній файл», ставить spring.config.location, і раптом базовий application.yml усередині jar перестає брати участь. У підсумку частина властивостей зникає, і здається, що застосунок зламався. На практиці найчастіше вам потрібні spring.config.additional-location або акуратний spring.config.import.

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

Помилка № 3: намагатися задати spring.config.location всередині application.yml.
Це логічна петля, а не налаштування. Параметри spring.config.location і spring.config.additional-location читаються дуже рано — ще до того, як Boot розібрався з вашими конфігураційними файлами. Тому їх задають через env vars, -D або аргументи запуску. Якщо ви покладете це у файл, який має бути знайдений за тими самими правилами, у вас вийде: «Шукаю карту, щоб знайти карту».

Помилка № 4: плутати fixed location і relative import, а потім дивуватися, чому не знаходить файл.
Якщо ви написали spring.config.import: optional:extra/runtime.yml, ви свідомо обрали відносний шлях. Він залежить від того, де знаходиться файл, який оголосив import. Якщо ви перенесли файл або змінили структуру каталогів, відносний шлях теж зміниться. Для контейнера частіше простіше fixed location (file:/config/runtime.yml), щоб не думати, «відносно чого».

Помилка № 5: імпортувати занадто багато файлів і побудувати конфігураційний лабіринт.
spring.config.import — зручний інструмент, але він не повинен перетворюватися на роман «Війна і мир» із 12 глав. Коли імпортів стає багато, у новачка ламається головна ціль дня: передбачуваність. У межах курсу краще тримати ланцюжок коротким: один зовнішній runtime-файл або один додатковий каталог — і все.

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