JavaRush /Курсы /Docker for Spring /Property sources Spring Boot

Property sources Spring Boot

Docker for Spring
11 уровень , 1 лекция
Открыта

1. Порядок property sources

Если вы хоть раз меняли server.port в application.yml, запускали сервис и видели, что он упрямо стартует на другом порту — поздравляю: вы уже встретились с тем, что Spring Boot конфигурация живёт не в одном файле, а в нескольких «слоях». В Docker-среде это всплывает ещё быстрее, потому что часть значений приходит «снаружи» контейнера, и разработчик начинает лечить не ту причину: правит файл, пересобирает image, ругается на Docker, потом на Spring, потом на себя (это нормальный путь, не стесняйтесь).

Здесь нас интересует не конкретный синтаксис -e, -D или --..., а фундамент: Spring Boot всегда собирает итоговые значения из нескольких источников, и у этих источников есть порядок приоритетов. Как только вы понимаете этот порядок, конфигурация перестаёт быть мистикой и становится инженерной задачей: «где именно задан ключ и какой источник его переопределил».

Property sources и Environment

В Spring Boot слово property — это просто пара ключ=значение. Примеры знакомые: server.port=8080, spring.profiles.active=standalone, app.mode=postgres. А property source — это конкретное место, откуда такие пары могут прийти: конфигурационный файл, переменные окружения, аргументы запуска, системные свойства JVM и так далее. Важно здесь не количество источников (их много), а механика: Boot складывает их в одну «копилку», но не по принципу «что раньше прочитал — то и правда», а по принципу приоритета.

Внутри приложения роль «главного бухгалтера конфигурации» играет объект Environment. Почти весь Spring (и ваш код тоже) в итоге спрашивает именно у него: «какое значение у ключа X?». И самое интересное: Environment отвечает уже итоговым значением, без объяснений «кто именно победил». Ему всё равно, пришло значение из файла, env var или аргумента запуска — он возвращает победителя.

Вот минимальный фрагмент логики, который удобно держать в любом временном диагностическом probe. Нам здесь важен не новый endpoint как таковой, а сам факт: итоговое значение мы спрашиваем у Environment.

String mode(Environment env) {
    return env.getProperty("app.mode", "standalone");
}

Здесь standalone — просто дефолт для нашего учебного app.mode, если ключ не задан нигде. Меняя файл, env vars или аргументы запуска, вы всё равно будете получать ответ именно отсюда: из итогового Environment.

2. Модель «слоёного пирога»

Чтобы не утонуть в деталях, удобно представить конфигурацию Spring Boot как слоёный пирог (или «селёдку под шубой», если вам ближе отечественные стандарты). Внизу лежит базовая конфигурация из файлов (например, application.yml внутри jar). Поверх неё могут лечь переменные окружения контейнера. Ещё выше — системные свойства JVM. И на самой верхушке — аргументы приложения (--key=value). Когда один и тот же ключ встречается в нескольких слоях, побеждает верхний слой. При этом побеждает не «весь файл целиком», а конкретный ключ.

Эту идею удобно зафиксировать схемой:

flowchart TB
    %% Снизу — базовые значения и "дефолты", сверху — самые приоритетные переопределения.
    A["Код (default в @Value / getProperty)"] --> B["Файлы config data (application.yml)"]
    B --> C["Env vars (переменные окружения)"]
    C --> D["JVM system properties (-Dkey=value)"]
    D --> E["Application args (--key=value)"]

    %% Важно: конфликт решается на уровне КЛЮЧА, а не на уровне "файла целиком".
    note["Один и тот же ключ может быть в нескольких слоях. Выигрывает самый верхний источник, где он задан."]
    E --> note

Обратите внимание на самый нижний слой. Когда вы пишете ${app.mode:standalone} или getProperty("app.mode", "standalone"), вы задаёте дефолт в коде, который используется только если ключ не пришёл ниоткуда. Это не property source в привычном смысле, но для мышления новичка это полезно: «даже если я ничего не передал, приложение не обязано падать — оно может иметь разумные значения по умолчанию».

4. Упрощённая лестница приоритетов

В реальном Spring Boot источников больше, чем нужно для ежедневного container workflow, и это нормально. Но для контейнерного сценария нам достаточно запомнить рабочую лестницу, которая покрывает 95% случаев в повседневной жизни Java-разработчика:

Уровень (слабый → сильный) Что это Пример ключа Что обычно хранит
Самый слабый Default в коде ${app.mode:standalone} «Пусть хоть как-то работает без настроек»
База application.yml (config data) server.port: 8080 Значения «по умолчанию для проекта»
Сильнее Env vars SERVER_PORT=8081 Окруженческие переопределения (контейнер, dev-машина)
Ещё сильнее System properties JVM -Dserver.port=8082 Запусковые параметры JVM / процесса
Самый сильный (в нашей упрощённой модели) Application args --server.port=8083 Явные аргументы запуска приложения

В этой таблице нет цели заставить вас выучить все формы и нюансы. Цель проще: если значение «не такое, как в application.yml», значит где-то выше по лестнице оно переопределено. И чем ближе вы к Docker, тем чаще верхние уровни участвуют в жизни.

Чтобы не смешивать precedence с профильной механикой и инфраструктурными режимами, дальше держим простой учебный ключ app.mode. Представим очень типичную ситуацию. В application.yml внутри jar у нас выставлен дефолтный порт и дефолтный режим:

# Базовые значения проекта: работают "из коробки", если ничего не переопределять при запуске.
server:
  # Дефолтный порт для локального запуска и для контейнера без дополнительных настроек.
  port: 8080

app:
  # Дефолтный режим приложения; дальше мы будем переопределять его сверху.
  mode: standalone

Это отличный baseline: сервис стартует локально, в контейнере, где угодно. Но как только вы в какой-то момент запускаете контейнер с переопределением порта (или профиля), application.yml перестаёт быть «последней инстанцией». Он остаётся базой, но не победителем.

5. Переопределение по ключу

Одна из самых обманчивых вещей в конфигурации — ощущение, что если «победил источник X», то он победил всё. На самом деле Spring Boot выбирает победителя для каждого ключа отдельно. Это очень удобно: вы можете переопределить ровно одно значение, не переписывая весь конфиг. Но это же и источник путаницы: вы правите application.yml, ожидая, что «всё станет как там», а меняется только то, что не перекрыто сверху.

Давайте расширим любой временный runtime-probe так, чтобы он показывал сразу пару значений. Нам здесь важен не новый controller, а сам факт: можно одновременно увидеть два ключа и сразу понять, что победило по каждому.

// Если у вас уже есть диагностическая GET /api/runtime ручка, её удобно расширить так:
@Value("${server.port}")
private String port;

@Value("${app.mode:standalone}")
private String mode;

@GetMapping("/api/runtime")
String runtime() {
    return mode + " @ " + port;
}

Такой метод удобно повесить на вашу диагностическую GET /api/runtime ручку. Важна именно идея: мы смотрим сразу два ключа и видим победителя по каждому.

Теперь представим три запуска (можно локально, можно в контейнере — принцип одинаковый). Я специально покажу команды через java -jar, потому что в контейнере мы именно так и стартуем, даже если собрали образ multi-stage’ом.

Запуск №1: ничего не переопределяем, берём базу из application.yml.

# Базовый запуск: берём значения из application.yml (и дефолты из кода, если ключа нет).
java -jar app.jar

Тогда вызов:

GET /api/runtime

вернёт примерно:

standalone @ 8080

Запуск №2: переопределяем только app.mode через аргумент запуска (да, это уже «верхний слой»).

# Переопределяем ровно один ключ через application args: он перекроет файл.
java -jar app.jar --app.mode=postgres

Тогда:

standalone @ 8080   (не будет)
postgres @ 8080     (будет)

Обратите внимание: порт не менялся, потому что мы не трогали server.port. И вот это — нормальная модель: конфигурация не «переключается целиком», она собирается по ключам.

Запуск №3: устраиваем маленький конфликт: app.mode задаём и как system property, и как application arg.

# Конфликт намеренный: один и тот же ключ задан и через -D, и через --args.
# В упрощённой лестнице приоритетов --app.mode окажется "выше" и победит.
java -Dapp.mode=fromSystem -jar app.jar --app.mode=fromArg

Результат будет:

fromArg @ 8080

И это момент, который нужно принять как правило игры: если вы задали один и тот же ключ в нескольких местах, более приоритетный источник перекроет менее приоритетный. Никаких «оба значения будут учтены», «сольются», «выберется случайное». Будет просто победитель. А проигравший останется в истории как причина ваших будущих вопросов «почему оно игнорируется».

6. Практика: конфликты и отладка

Пересборка image и конфликты конфигурации

Когда вы в контейнерном мире, первая реакция на странное поведение часто такая: «Наверное, образ старый. Надо пересобрать». Это иногда правда (если вы поменяли код), но при конфликтах конфигурации пересборка — как попытка лечить температуру заменой градусника: вроде действие есть, а смысла мало. Порядок property sources от пересборки не меняется, и если значение перекрыто сверху, оно и останется перекрытым.

Самый частый сценарий выглядит так. Внутри jar лежит application.yml, вы меняете там server.port: 8080, пересобираете image, запускаете контейнер — а порт всё равно «не тот». И дальше начинается гадание, хотя нужно просто задать один вопрос: «А не передаю ли я server.port где-то ещё?». В Docker-реальности это легко: ENV в Dockerfile, -e в docker run, переменные окружения из вашей IDE или терминала, аргументы запуска в ENTRYPOINT/CMD. Всё это может перекрывать файл, и пересборка тут ни при чём.

Поэтому полезная дисциплина звучит так: если изменился только конфиг запуска — не пересобираем образ, ищем победивший источник; если изменился код/артефакт — пересобираем образ. Это ровно та граница, которую мы хотим закрепить в модуле 3.

Отладка: «ищем, где задан ключ»

Когда конфигурация «не совпадает с ожиданиями», очень хочется прыгнуть в самый заметный слой — обычно это application.yml — и чинить там. Но правильнее начинать с другого: выбрать один конкретный ключ (например, server.port) и устроить маленькое расследование: где он вообще может быть задан? На этом этапе важно не расширять проблему до «вся конфигурация сломана», потому что она почти никогда не сломана вся — обычно спорят 1–2 ключа.

Практический путь выглядит так. Сначала вы убеждаетесь, что видите итоговое значение. Для этого мы и делали /api/runtime: это быстрый ответ, который показывает «что реально получилось». Затем вы проверяете базовый слой: что написано в application.yml. После этого вы вспоминаете лестницу приоритетов и честно задаёте себе вопрос: «а не переопределяю ли я это через env vars, через -D или через --...?». Если приложение работает в контейнере, вы добавляете ещё один важный слой: «что именно я передал контейнеру при старте?».

Когда речь о контейнере, очень помогает то, что мы изучали в ранних днях курса про Docker CLI. Команда docker inspect умеет показать, с какими переменными окружения был запущен контейнер. И это не «редкая девопс-команда», а банальный фонарик в темноте. Если вы видите внутри Env что-то вроде SERVER_PORT=8081, то вопрос «почему не 8080 из файла» закрывается сам собой.

Для закрепления мысли — ещё одна маленькая схема, на которую можно мысленно опираться, когда начинается конфигурационная паника:

flowchart TD
    A["Симптом: значение не то"] --> B["Фиксируем ключ (например, server.port)"]
    B --> C["Смотрим итог (Environment / /api/runtime / логи)"]
    C --> D["Ищем ключ в application.yml"]
    D --> E["Ищем, не переопределён ли ключ сверху: env vars → -D → --args"]
    E --> F["Если контейнер: проверяем docker inspect / команду запуска"]
    F --> G["Убираем дубли / оставляем один главный источник"]

Ключевая идея в конце: ваша цель не «угадать правильный источник», а сделать так, чтобы у ключа был один понятный хозяин. Иначе конфиг превращается в спорт «кто громче крикнет» — а громче обычно кричит тот, кто был задан последним и сверху.

7. Типичные ошибки

Ошибка №1: считать application.yml абсолютной истиной.
Файл application.yml — это базовый слой, отличный для дефолтов проекта, но он не может быть «абсолютной правдой», потому что Spring Boot специально создан так, чтобы его можно было переопределять при запуске. В Docker-сценариях это особенно заметно: окружение контейнера часто перекрывает значения из jar, и это ожидаемое поведение, а не баг.

Ошибка №2: лечить конфигурационную проблему пересборкой image.
Если вы поменяли только значения запуска, пересборка образа обычно не изменит ничего, кроме вашего настроения и времени ожидания. Конфликт в конфигурации решается не docker build, а поиском победившего источника и удалением дублей. Пересборка имеет смысл, когда меняется код или артефакт, а не когда вы спорите с SERVER_PORT.

Ошибка №3: задавать один и тот же ключ сразу в трёх местах «на всякий случай».
Иногда кажется логичным: «пусть будет и в application.yml, и в env var, и ещё в аргументах запуска — вдруг что». На практике это приводит к тому, что через неделю никто (включая вас) не помнит, кто реально влияет на итог. Правильная привычка — держать дефолт в application.yml, а переопределять точечно и осознанно одним выбранным способом.

Ошибка №4: мыслить конфигурацией как “переключателем режима целиком”.
Новички часто ожидают, что «если я включил какой-то режим», то всё приложение начнёт жить по одному файлу, а остальные слои исчезнут. Но Spring Boot переопределяет значения по ключам, поэтому вы можете получить «гибрид»: один ключ взят из файла, другой — из env var, третий — из аргументов запуска. Это не аномалия, это нормальная механика, которую нужно держать в голове.

Ошибка №5: пытаться разбирать конфигурацию “по месту”, а не “по ключу”.
Очень частая картина: разработчик долго смотрит только в Dockerfile или только в application.yml и не может понять, почему значение не меняется. Конфигурация в Boot решается по ключу, значит и расследование должно идти по ключу: “где задан server.port?”, “где задан spring.profiles.active?”. Как только вы переходите на эту оптику, хаос резко уменьшается.

1
Задача
Docker for Spring, 11 уровень, 1 лекция
Недоступна
Значение из `application.yml` и переопределение через env var
Значение из `application.yml` и переопределение через env var
1
Задача
Docker for Spring, 11 уровень, 1 лекция
Недоступна
Переопределение только одного ключа
Переопределение только одного ключа
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ