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” понятный baseline, который всегда запускается, а рядом иметь маленький файл 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 (или profile-specific файл) и говорите: “Если существует такой-то внешний файл — дочитай его тоже”. В контейнерном мире это удобно, потому что вы не пересобираете образ, а всего лишь даёте приложению дополнительный слой настроек.
Где объявлять import
Очень важно не запутаться “что где пишется”. spring.config.import можно объявить внутри конфигурационного файла, то есть прямо в application.yml (который у нас лежит в src/main/resources и попадает внутрь jar). Тогда получается приятный эффект: образ и jar всегда содержат baseline, а внешний файл становится “опциональной надстройкой”.
Представим, что в нашем сервисе уже есть свойство 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 будет считать, что импортируемый ресурс обязан существовать, и если его нет — старт может закончиться ошибкой. Для учебного проекта это почти всегда неприятно: вы хотите, чтобы baseline запускался “из коробки”, без того, чтобы студент сначала создавал секретные файлы в секретных местах.
Поэтому в нашем курсе хорошая дисциплина такая: если импортируемый файл — это “опциональный слой”, то в 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, значение берём из итоговой конфигурации
}
}
Если такого setup в проекте нет, добавьте обычное сканирование или регистрацию @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 заменяет дефолты
Под “default locations” мы сейчас понимаем тот самый стандартный набор, где Boot обычно ищет application.yml и application-{profile}.yml: внутри jar (classpath) и рядом с запуском (current dir, config/, и т.д.). Когда вы используете spring.config.location, вы как будто говорите: “Не надо искать конфиг по стандартным правилам. Я сам указываю, где он лежит”.
Отсюда главный practical-вывод. spring.config.location — штука сильная, но опасная для новичка. Она полезна, когда вы точно хотите контролировать окружение и понимаете, что делаете, но она может привести к ситуации: “Почему приложение вдруг перестало видеть application.yml, который я положил в src/main/resources?” Ответ будет неприятно простой: потому что вы сами попросили Boot не смотреть туда.
В учебном проекте такие “выстрелы в ногу” особенно обидны, потому что вы теряете baseline запуска. Поэтому в рамках курса мы относимся к spring.config.location как к инструменту “по делу”, а не как к настройке “на всякий случай”.
Как задавать location снаружи
Теперь важная механика, которую легко понять на бытовом уровне. Чтобы Spring Boot нашёл конфигурацию, ему сначала нужно понять, где её искать. А значит, параметры “где искать” должны прийти раньше, чем сами конфиги. Поэтому spring.config.location (и spring.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 из прошлого дня):
# Environment variable: часто самый удобный путь для контейнеров
SPRING_CONFIG_LOCATION=optional:file:/config/
Ключевой момент здесь не в синтаксисе, а в инженерной идее: эти свойства читаются очень рано, поэтому они должны быть доступны ещё до того, как Boot начнёт “листать” ваши application-файлы.
Когда location уместен
Чтобы не демонизировать инструмент, давайте честно обозначим сценарий, где он нормален. Представьте, что вы запускаете один и тот же jar в окружении, где вообще не хотите использовать внутренние дефолты (например, потому что вы делаете “жёстко заданный конфиг”, и базовые значения в jar вам не нужны). Тогда location позволяет сказать: “Моя конфигурация — только в этой директории”.
Но для нашего учебного “контейнерно-зрелого” сервиса чаще подходит более мягкая история: оставить baseline внутри jar и поверх него аккуратно добавить внешние overrides. И вот здесь чаще побеждает spring.config.additional-location, а не location.
4. spring.config.additional-location: добавляем путь поиска
Если spring.config.location — это “я переписываю правила игры”, то spring.config.additional-location — это “я добавляю ещё один путь поиска, но не выкидываю стандартное поведение”. Для учебного проекта и для команды, которая хочет предсказуемости, это обычно более безопасный и дружелюбный инструмент. Он помогает держать два мира вместе: конфиги внутри jar как baseline и внешние конфиги как аккуратный оверлей.
Разница 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/
Тогда студент может запустить сервис вообще без внешней директории, и всё будет работать на baseline конфиге внутри jar. А когда внешняя директория появится (как осознанный инструмент, а не обязательная точка входа) — она начнёт влиять на конфигурацию.
5. Шпаргалка выбора механизма
Когда на вас смотрят три похожих названия, мозг новичка обычно делает честную попытку “выучить наизусть”. Но в реальной разработке побеждает не память, а правильный вопрос. В нашем случае вопрос звучит так: “Я хочу добавить небольшой слой поверх существующего конфига, или хочу полностью заменить маршрут поиска конфигурации?”
Чтобы упростить выбор, вот таблица, которую можно держать в голове (и не стесняться возвращаться к ней):
| Механизм | Что вы говорите Boot | Когда обычно подходит | Что легко сломать |
|---|---|---|---|
| spring.config.import | “Из этого конфига дочитай ещё вот этот файл” | Когда нужен один конкретный дополнительный файл (часто runtime.yml) и вы хотите управлять этим прямо из application.yml | Если забыть optional: и файла нет — старт может упасть |
| spring.config .additional-location | “Ищи application-файлы ещё и в этой директории, но дефолты оставь” | Когда нужен внешний application.yml/application-{profile}.yml как оверлей к baseline внутри jar | Если вы ожидаете, что это “замена”, а это “добавка”, можно удивиться итоговому precedence |
| spring.config .location | “Ищи конфиг только по этому маршруту (по сути — перепиши маршрут поиска)” | Когда вы осознанно хотите отключить default locations и жить только на внешнем конфиге | Очень легко отключить classpath-конфиг и потерять baseline |
Если коротко в стиле “как бы я объяснил другу”: 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-файл или одна дополнительная директория — и всё.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ