JavaRush /Курсы /Spring Boot /Карта PropertySource

Карта PropertySource

Spring Boot
14 уровень , 0 лекция
Открыта

1. Карта источников конфигурации

Представьте ситуацию: вы честно меняете app.catalog.title в application.yaml, сохраняете файл, запускаете сервис… а заголовок в ответах всё равно старый. Первая мысль — «IDE не сохранила», вторая — «Spring опять магия», третья — «я точно программист, или просто удачливый печататель YAML?». На самом деле проблема почти всегда банальна: значение берётся не из того места, которое вы смотрите.

Spring Boot умеет собирать конфигурацию из нескольких источников одновременно, и если один и тот же ключ задан в нескольких местах, приложение всё равно увидит ровно одно итоговое значение. Чтобы не лечить симптомы, нам нужна нормальная mental model: какие бывают источники, как они складываются и почему одно значение выигрывает у другого.

PropertySource и источники свойств

Когда вы слышите «конфигурация Spring Boot», мозг обычно рисует один файл: application.yaml. Это нормально, мы все так начинали. Но в терминах Spring это слишком узко. PropertySource — это просто «место, откуда можно взять значение по ключу». Это может быть YAML-файл, переменная окружения ОС, параметр JVM, аргумент командной строки или даже «JSON-блок, спрятанный в переменной окружения». Файл — лишь один из вариантов.

Важно поймать простую мысль: Boot не читает «какой-то один правильный источник», он собирает слойный пирог. Каждый слой — это PropertySource. Слои накладываются друг на друга, и в итоге получается одна итоговая картина мира, из которой уже читает ваш код. Поэтому вопрос «в каком файле лежит значение» часто неправильный. Правильный вопрос звучит так: «какое итоговое значение видит приложение в runtime, и какой источник его победил?».

Чтобы ощутить это руками, полезно рассматривать конфигурацию как огромную Map<String, String>, но собранную из разных под-карт. Spring Boot выступает в роли аккуратного сборщика: он берёт эти под-карты, выстраивает их в определённом порядке и говорит: «если ключ встречается несколько раз — победит тот, кто стоит выше».

2. precedence и приоритеты источников

Слово precedence в этой теме звучит страшнее, чем оно есть на самом деле. Это не «тайная магическая настройка», а обычный порядок приоритетов: кто сильнее, тот и прав. Если один и тот же ключ присутствует в нескольких источниках, Boot не устраивает демократию, не проводит голосование и не спрашивает совета у вашего кота. Он берёт значение из более приоритетного источника — и всё.

Удобнее всего думать о precedence как о стопке прозрачных плёнок. Внизу лежит базовый слой — значения по умолчанию. Выше накладываются слои, которые уточняют поведение. Если наверху нарисовано новое значение для того же ключа, нижнее просто перекрывается. Это нормально и даже полезно: вы можете оставить хороший дефолт в приложении, внутри jar, а при запуске на другой машине перекрыть отдельные значения без пересборки.

Вот упрощённая схема. Мы пока специально не уходим в точный порядок всех редких источников — нам важна карта, а не справочник на 40 страниц.

flowchart TB
    A["Packaged config (внутри приложения)"] --> E["Spring Environment (итоговые значения)"]
    B["External config (рядом с запуском)"] --> E
    C["Launch-time overrides (параметры запуска)"] --> E
    E --> F["Ваш код читает env.getProperty(...)"]

Здесь ключевая идея не в том, что файлы плохие, а параметры запуска хорошие. Идея в том, что у конфигурации есть слои ответственности. Файл внутри приложения обычно задаёт базовый дефолт. Внешний файл — настройку для конкретного окружения. А параметры запуска — разовый override здесь и сейчас.

Если вы поймёте этот принцип, большинство конфигурационных мистических багов внезапно перестанут быть мистическими. Они станут скучными. А скучное — это прекрасно, потому что скучное чинится по инструкции.

3. Packaged и external конфигурация

Теперь про то, что чаще всего реально встречается в проектах: конфигурация внутри приложения и конфигурация снаружи. На уровне нашей mental model достаточно помнить два слова: packaged и external.

Packaged config — это то, что лежит в вашем проекте в src/main/resources и уезжает внутрь jar. Например, src/main/resources/application.yaml. Когда вы собираете приложение, этот файл становится частью артефакта. Это удобно, потому что у приложения появляется встроенный дефолт: оно может стартовать даже без внешних файлов.

External config — это то, что лежит рядом с запуском приложения, условно «файл рядом с jar» или в типовой папке конфигурации. Идея простая: вы не хотите пересобирать jar, чтобы поменять заголовок, порт или флаг режима обслуживания. Поэтому вы подкладываете внешний файл, и Boot использует его как более сильный слой по сравнению со встроенным.

Выглядит это максимально буднично: один и тот же ключ может встретиться в двух местах, и это не ошибка, если вы чётко понимаете, какой из них — дефолт, а какой — override.

# packaged: src/main/resources/application.yaml
app:
  catalog:
    title: "Catalog from jar"

# если ключ повторяется, победит более приоритетный слой (например, внешний файл)
# external: условно ./config/application.yaml рядом с запуском
app:
  catalog:
    title: "Catalog from external file"

Если приложение увидело "Catalog from external file", это не «оно проигнорировало ваш application.yaml». Оно его прочитало. Просто сверху лежал слой, который перекрыл значение.

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

4. Environment: итоговая конфигурация

Слово Environment в Spring почти гарантированно вызывает путаницу у новичков, потому что в голове уже есть environment variables операционной системы. Да, ОС тоже даёт переменные окружения. Но Spring Environment — это другое. Это Spring-объект, который хранит итоговый результат сборки всех PropertySource с учётом precedence. То есть это «всё, что приложение знает о своих настройках», в одном месте.

Эта идея сильно упрощает жизнь: вашему коду не нужно знать, откуда пришло значение. Из файла? Из переменной окружения? Из аргумента запуска? Неважно. Код читает Environment — и получает финальный ответ. А когда что-то работает не так, вы не спорите о файлах на глаз, а идёте в Environment и проверяете, что реально видит runtime.

В catalog-service мы можем, в учебных целях, добавить маленький раннер, который выводит пару ключей и показывает, что победило. Сейчас мы используем System.out.println, потому что отдельная дисциплина логирования у нас будет позже, а сегодня нам важнее увидеть сам принцип.

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
class PropertyProbeRunner implements ApplicationRunner {

    private final Environment environment;

    PropertyProbeRunner(Environment environment) {
        // Spring сам внедрит Environment, который уже собрал все PropertySource с учётом precedence
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Читаем итоговое значение: не важно, пришло оно из YAML, env vars или аргумента запуска
        // "<missing>" — явный дефолт, чтобы отличать "ключ отсутствует" от "ключ есть, но пустой"
        String title = environment.getProperty("app.catalog.title", "<missing>");

        // Для демонстрации принципа используем System.out.println (логирование будет отдельной темой)
        System.out.println("app.catalog.title = " + title); // app.catalog.title = Spring+ Catalog
    }
}

Этого одного временного probe-runner для таких проверок вполне достаточно. Дальше, если захотите посмотреть другой ключ или сравнить другой канал запуска, проще менять содержимое run(...), чем держать в проекте коллекцию похожих стартовых помощников: иначе startup быстро превращается в шум.

Обратите внимание на две детали. Во‑первых, мы не парсим YAML руками и не читаем файлы через Files.readString(...). Во‑вторых, мы используем дефолт "<missing>", чтобы явно отличать ситуацию «ключ не найден» от ситуации «ключ найден, но пустой». Это маленькая привычка, но она очень спасает нервы.

Ещё одна полезная возможность Environment: он умеет простую конвертацию типов. Пока мы не привязываем конфигурацию к отдельным Java-объектам, но даже на базовом уровне можно попросить Spring вернуть значение как Integer или Boolean.

import org.springframework.core.env.Environment;

class SimpleReads {

    private final Environment env;

    SimpleReads(Environment env) {
        // Environment здесь — тот же итоговый "слепок" конфигурации, собранный из всех источников
        this.env = env;
    }

    void show() {
        // Spring попробует сконвертировать строковое значение в нужный тип
        // Третий аргумент — дефолт, который будет использован, если ключ не найден
        Integer limit = env.getProperty("app.catalog.max-featured-count", Integer.class, 4);
        Boolean maintenance = env.getProperty("app.catalog.maintenance-mode", Boolean.class, false);

        System.out.println("limit = " + limit);             // limit = 4
        System.out.println("maintenance = " + maintenance); // maintenance = false
    }
}

Это всё ещё сырой способ работы с конфигурацией, но для сегодняшней темы он идеален: он подчёркивает, что главный объект, который видит приложение, — Environment, а не отдельный файл.

5. Мини-кейс: один ключ в разных источниках

Давайте соберём в голове максимально типичный и максимально жизненный кейс: один ключ app.catalog.title задан в нескольких местах. Например, в src/main/resources/application.yaml вы оставили базовое значение, потому что проект должен стартовать «из коробки». Затем вы запускаете приложение на другой машине и хотите другой заголовок — подкладываете внешний конфиг. А потом, в момент диагностики, вы делаете разовый запуск с параметром, потому что «надо проверить вот прямо сейчас, не трогая файлы».

Мы сейчас не углубляемся в конкретный синтаксис и точный порядок всех каналов запуска — это отдельные темы следующих лекций дня. Здесь важен сам принцип: ключ один, источников несколько, итоговое значение одно.

Схематично это можно представить так:

Где задано значение Пример значения Роль
Packaged config (внутри jar) Catalog from jar базовый дефолт, чтобы проект стартовал
External config (рядом с запуском) Catalog from external file настройка для конкретной среды/машины
Launch-time override (при запуске) Catalog from CLI разовая корректировка «на этот запуск»

И именно поэтому при отладке всегда полезно задавать себе вопрос не «в каком файле у меня написано…», а «какой итоговый слой сейчас победил?». Потому что вы можете смотреть в packaged application.yaml до посинения, но если сверху есть override, ваш взгляд будет абсолютно честным, но бесполезным.

В реальном проекте это и есть причина, почему человек говорит: «Я же поменял!», а приложение отвечает: «Да, я видел. И всё равно сделал по‑своему». Boot не вредничает — он просто следует правилу precedence.

6. Читаемая конфигурация при множестве источников

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

Здоровая стратегия начинается с простого разделения ролей. Packaged config внутри проекта должен быть местом, где лежат разумные дефолты и то, что делает приложение самодостаточным. Внешняя конфигурация нужна, когда вы хотите менять поведение без пересборки и без правки исходников. Параметры запуска полезны для диагностики и разовых переключений, когда вы не хотите редактировать файлы вообще.

Если держать это разделение в голове, вы интуитивно перестаёте дублировать ключи «просто потому что можно». Вы начинаете задавать вопрос: «это значение — дефолт или override?». И конфигурация перестаёт быть мусорной кучей.

Ещё одна маленькая, но очень практичная мысль: когда вы отлаживаете странное поведение, не пытайтесь сразу найти «тот самый файл». Лучше найти ключ, а потом проверить, какое значение он реально принимает в Environment. Это даже психологически проще: спорить с реальностью тяжело, а Environment — это реальность в формате String.

Отсюда вырастает и следующий практический вопрос: каким каналом запуска вы вообще перебиваете этот базовый дефолт — внешним env var, -D или --key=value. Как только это начинаешь проверять не по файлам, а по итоговому значению в Environment, половина магии сразу исчезает.

7. Типичные ошибки: mental model property sources

В этой теме ошибки редко выглядят как «не компилируется». Чаще это ошибки мышления: приложение работает, но «не слушается», и вы начинаете думать, что оно игнорирует ваши настройки. На практике оно просто честно следует precedence и собирает значения из нескольких источников. Давайте зафиксируем самые частые грабли, чтобы вы наступали на них реже и более осознанно.

Ошибка №1: считать application.yaml единственным источником правды.
Новички часто мыслят так: «если я вижу значение в YAML, значит приложение обязано его использовать». Но Boot видит не один YAML, а набор источников. Поэтому правильная привычка — разделять «где значение объявлено» и «какое значение стало итоговым». Итог всегда живёт в Environment, а не в открытом в IDE файле.

Ошибка №2: путать OS environment variables и Spring Environment.
Переменные окружения операционной системы — это один из источников. Spring Environment — это объект, который содержит итог, уже собранный из разных источников. Если смешать эти понятия, вы начинаете ожидать от Environment поведения терминала, а от терминала — поведения Spring. Кончается это обычно тем, что вы начинаете «переименовывать переменные, пока не заработает», вместо того чтобы проверять итоговое значение.

Ошибка №3: диагностировать поведение по одному файлу, игнорируя слои.
Самый распространённый сценарий конфликта: ключ есть в packaged конфиге, но сверху его перекрыли. Вы смотрите в packaged файл и не понимаете, почему не меняется поведение. Лечится это просто: при проблеме с конфигурацией первым делом фиксируется точный ключ и проверяется его итоговое значение в runtime, а не проводится чемпионат по чтению YAML глазами.

Ошибка №4: дублировать ключи в нескольких местах без роли и договорённостей.
Иногда люди «на всякий случай» копируют один и тот же ключ в несколько файлов: в application.yaml, в какой-то внешний конфиг, в IDE Run Configuration. Сначала всё «как-то работает», а потом любое изменение начинает вести себя непредсказуемо. В layered конфигурации ключ может встречаться несколько раз, но у каждого появления должна быть понятная роль: дефолт, внешний override или разовый запуск.

Ошибка №5: думать, что раз значение видно после старта, значит оно всегда влияло на поведение старта.
Это тонкий момент. Пока достаточно запомнить принцип: некоторые настройки важны очень рано, и у них есть фактор времени. Поэтому фраза «я вывел значение через Environment после старта» не всегда равна «оно точно участвовало в старте». Это не баг, это устройство процесса запуска.

1
Задача
Spring Boot, 14 уровень, 0 лекция
Недоступна
Итоговое значение из Environment
Итоговое значение из Environment
1
Задача
Spring Boot, 14 уровень, 0 лекция
Недоступна
Внешний файл сильнее packaged-конфига
Внешний файл сильнее packaged-конфига
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ