JavaRush /Курсы /Spring Boot /Конфликты precedence в catalog-service

Конфликты precedence в catalog-service

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

1. Конфиг задан, а поведение другое

Если бы Spring Boot умел читать мысли разработчика, половина профессии «backend-инженер» исчезла бы как класс. Но Boot читает не мысли, а источники конфигурации, и делает это по правилам precedence. Поэтому типичный баг звучит так: «Я точно поменял значение в YAML, а оно точно не применилось». На практике это чаще всего не “Boot сломался”, а “в цепочке источников есть более сильный победитель, о котором вы забыли”.

Представьте, что у вас в catalog-service есть свойство app.catalog.title. В src/main/resources/application.yaml оно стоит как "Spring+ Catalog", вы меняете его на "My Local Catalog", перезапускаете — и внезапно видите в логах/на странице старое значение. Раздражает, да. Но это отличный повод включить «режим детектива»: у конфигурации есть несколько потенциальных “подозреваемых”, и наша задача — найти того, кто реально победил.

К этому моменту картина уже полная: значение может приехать из файлов, env vars, -D, --... или JSON, а часть свойств ещё и чувствительна к моменту чтения. Поэтому финальный вопрос уже не «что такое precedence», а «по какому маршруту быстро вычислить виновника, когда поведение расходится с ожиданием».

Здесь помогает не ещё один спор с YAML, а короткий алгоритм: фиксируем ключ, смотрим effective value, а дальше идём либо по лестнице источников, либо по timing-ветке для ранних свойств.

2. Алгоритм расследования

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

Ниже — практичная блок-схема. Это не «идеальная теория Spring», а рабочий путь для типовых проблем нашего дня: packaged/external config, env vars, system props, command-line args, SPRING_APPLICATION_JSON и ранние свойства старта.

flowchart TD
  A["Симптом: значение 'не то' или раннее поведение старта не совпало с ожиданием"] --> B["Фиксируем точный ключ и ожидаемое значение/поведение"]
  B --> C["Смотрим effective value через Environment в runtime"]
  C --> D{"Effective value совпадает с ожиданием?"}
  D -- "Нет" --> E["Проверяем как приложение реально запущено (IDE/bootRun/терминал)"]
  E --> F["Идём по precedence сверху вниз: CLI → JSON → -D → env → external file → packaged file"]
  F --> G{"Ключ найден в более сильном источнике?"}
  G -- "Да" --> H["Исправляем/убираем override в сильном источнике"]
  G -- "Нет" --> I["Проверяем: ключ не задан, написан не так или спрятан в placeholder/default"]
  D -- "Да" --> J{"Симптом связан с ранним стартом? logging.*, spring.main.*"}
  J -- "Да" --> K["Проверяем timing: значение должно быть доступно до ранней стадии запуска"]
  J -- "Нет" --> L["Ищем причину уже не в precedence, а в другом месте приложения"]

Здесь важно разделить два разных класса проблем. Первый — wrong winner: effective value уже не то, и тогда мы ищем более сильный источник. Второй — right value, wrong timing: Environment уже показывает нужное значение, но раннее поведение старта всё равно не изменилось, потому что свойство попало слишком поздно.

Если держать эти две ветки отдельно, расследование резко становится короче. Иначе очень легко бесконечно перелистывать YAML в поисках «неправильного источника», хотя проблема вообще не в winner, а во времени применения.

3. Фиксируем ключ и ожидаемое значение

Перед тем как искать победивший источник, нужно сделать вещь, которая звучит слишком банально, чтобы быть важной: зафиксировать точное имя свойства. В конфигурации Spring Boot ошибка “одна буква не такая” встречается чаще, чем баги фреймворка. Причём это не всегда очевидно: в YAML у нас maintenance-mode, а в голове у новичка уже живёт maintenanceMode, и мозг говорит «ну это же почти одно и то же».

Для catalog-service мы договорились о namespaced-ключах под префиксом app.catalog.*. Это помогает: вы сразу понимаете, что catalog.title и app.catalog.title — вообще разные сущности, и второе намного более ожидаемо в нашем проекте. Но даже при таком префиксе остаются типичные ловушки: тире vs точка, лишний уровень вложенности, или попытка переопределять не тот ключ.

Очень практичный приём на время диагностики — вынести ключ в константу, чтобы не плодить “почти одинаковые строки” по коду. Это не обязательная архитектура, а «костыль-лупа» для расследования.

package com.example.catalogservice.support;

public final class AppProps {
    private AppProps() {}

    // Константы снижают риск опечатки в ключе свойства при диагностике.
    public static final String CATALOG_TITLE = "app.catalog.title";
}

Когда вы дальше выводите значение, вы уже не можете случайно написать app.catalog.tilte и потом час искать «почему не работает». Да, это смешно. И да — такое случается даже у опытных людей в пятницу вечером.

4. Проверяем значение в Environment

Ключевой переход в мышлении сегодня такой: мы перестаём спорить с файлами и начинаем разговаривать с приложением. Spring Boot может прочитать пять источников, десять раз переопределить одно и то же значение, применить placeholders — и в итоге всё равно останется один факт: какое значение видит приложение в Environment. Именно с него и начинается нормальная диагностика.

В catalog-service здесь снова достаточно того же временного probe-runner. Если для расследования хочется увидеть сразу несколько спорных ключей, просто расширьте его вывод на один запуск, а не оставляйте в проекте ещё один постоянный Runner.

package com.example.catalogservice.catalog.bootstrap;

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

@Component
class PropertyTraceRunner implements ApplicationRunner {

    private final Environment environment;

    PropertyTraceRunner(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void run(ApplicationArguments args) {
        // Диагностический вывод: показывает итоговое (effective) значение после всех override.
        // Важно: getProperty(...) может вернуть null, если ключ не задан ни в одном источнике.
        System.out.println("app.catalog.title="
                + environment.getProperty("app.catalog.title")); // например: CLI title
    }
}

Тут важно два нюанса. Во‑первых, Environment показывает итоговое значение независимо от того, пришло оно из YAML, env vars или command-line args. Во‑вторых, вы теперь можете сравнить «что я ожидаю» с «что реально получилось» без философии и догадок.

Если вы видите, что значение уже не то, значит конфликт реальный: где-то есть override. Если значение то самое, но поведение не изменилось — значит пора идти во вторую ветку алгоритма и проверять timing ранних свойств, а не продолжать спорить с precedence.

5. Проверяем команду запуска

Большинство конфликтов precedence рождаются не в YAML, а в момент запуска. И это логично: запуск — это точка, где можно «на лету» переопределить поведение без правки кода и ресурсов. Проблема в том, что запускаем мы приложение не всегда одинаково: иногда из IDE, иногда через ./gradlew bootRun, иногда через java -jar, а иногда — вообще «как-то» из вчерашней вкладки терминала.

Начните расследование с простого вопроса: какая была фактическая команда запуска? Для IDE это означает открыть Run Configuration и посмотреть, что там в Environment Variables и Program Arguments. Для bootRun — посмотреть, не передаёте ли вы --args. Для терминала — проверить, не висит ли у вас в окружении переменная, которую вы забыли, но она помнит вас (и напоминает о себе самым неприятным способом).

Ниже — три канала override, которые чаще всего «делают вид, что их нет», хотя именно они и победили:

# env var (Unix/macOS): влияет на конфиг через Environment variables
APP_CATALOG_TITLE="Catalog from env" java -jar app.jar

# system property (важно: ДО -jar): попадает в Java system properties (-D...)
java -Dapp.catalog.title="Catalog from -D" -jar app.jar

# command-line property: самый «громкий» канал среди этих трёх
java -jar app.jar --app.catalog.title="Catalog from CLI"

Если вы запускаете одновременно с env var и с CLI arg, угадайте, кто победит. Спойлер: тот, кто «громче» по precedence, а не тот, кого вы «больше уважаете».

6. Лестница precedence и поиск источника

Precedence полезнее всего воспринимать как лестницу. Вы стоите на верхней ступеньке (самый сильный источник) и спускаетесь вниз, пока не найдёте место, где ключ реально задан. И как только нашли — вы почти наверняка нашли победителя. Важно не пытаться держать всю документацию Boot в голове; для нашего дня достаточно базового порядка, который мы уже закрепили.

Вот рабочая «лестница» именно для источников, которые мы сегодня используем в catalog-service:

Сила Источник Как выглядит пример Типичный эффект
1 Command-line args --app.catalog.title=CLI title «Я точно задавал в YAML, но не работает»
2 SPRING_APPLICATION_JSON SPRING_APPLICATION_JSON='{"app":{"catalog":{"title":"JSON"}}}' Много значений одной строкой
3 Java system properties -Dapp.catalog.title=FromD Часто спрятано в IDE/скриптах
4 Environment variables APP_CATALOG_TITLE=FromEnv Часто «забыто» в терминале
5 External file config ./config/application.yaml Перекрывает packaged YAML
6 Packaged file config src/main/resources/application.yaml Базовый default внутри приложения

Если один и тот же ключ явно задан в нескольких местах, полезно на один запуск включить ещё более точечный helper: он показывает, в каком PropertySource этот конкретный ключ встречается первым. Это не универсальный detector на все случаи жизни, а быстрый инструмент именно для прямого same-key override.

package com.example.catalogservice.catalog.bootstrap;

import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;

@Component
class PropertySourcePeekRunner implements ApplicationRunner {

    private final ConfigurableEnvironment env;

    PropertySourcePeekRunner(ConfigurableEnvironment env) {
        this.env = env;
    }

    @Override
    public void run(org.springframework.boot.ApplicationArguments args) {
        // Идём по источникам сверху вниз по precedence именно для одного и того же ключа.
        // Как только ключ найден, для прямого override этого уже достаточно:
        // ниже источники содержат более слабые версии того же значения.
        for (PropertySource
   ps : env.getPropertySources()) {
            if (ps.containsProperty("app.catalog.title")) {
                System.out.println("title from: " + ps.getName()); // имя property source из Spring
                break;
            }
        }
    }
}

Если вы увидели что-то вроде commandLineArgs, спорить с YAML уже бессмысленно: YAML проиграл честный бой по правилам. Нужно не «ещё раз переписать YAML», а убрать или исправить более сильный override.

Плейсхолдеры, defaults и null в JSON

Но здесь важно не переоценить helper. В конфликте precedence есть две похожие на вид ситуации, но лечатся они по-разному. Первая: значение вообще не задано, и Boot берёт default (например, из placeholder или просто из файла). Вторая: значение задано, но его переопределили более сильным источником. Снаружи обе выглядят как «не то значение», но причины противоположные: в одном случае “не хватает данных”, в другом — “данных слишком много”.

Для прямого same-key override containsProperty("app.catalog.title") работает отлично. Но для placeholder/default-цепочек вроде ${APP_CATALOG_TITLE:Spring+ Catalog} этого уже мало: сначала фиксируем итог через Environment, а потом отдельно смотрим сам ключ внутри placeholder-а.

Возьмём популярный паттерн из YAML:

app:
  catalog:
    # Если переменная окружения APP_CATALOG_TITLE не задана — берём fallback "Spring+ Catalog"
    title: "${APP_CATALOG_TITLE:Spring+ Catalog}"

Если APP_CATALOG_TITLE не задан, Environment покажет Spring+ Catalog. Это не override, это fallback. Но если переменная задана, YAML уже сам «передаёт управление» наружу — и вы получаете значение из окружения. При этом важно помнить: даже если в YAML есть default, он не «побеждает» более сильные источники вроде CLI args. Default — это запасной парашют, а не броня.

Отдельная тонкость — SPRING_APPLICATION_JSON и ожидание “стирания”. Многие интуитивно думают: «Если я передам null, то это удалит значение». Но в контексте SPRING_APPLICATION_JSON null чаще воспринимается как «ключ отсутствует для переопределения», а не как «обнули нижний слой». Поэтому если вы пытаетесь “снять” значение, а оно не снимается — это может быть не баг, а неправильная модель ожиданий.

7. Кейсы из catalog-service

Сейчас мы возьмём три ситуации, которые выглядят очень жизненно: одна про app.catalog.title, одна про числовое ограничение, и одна про порт. Я специально выбираю простые свойства: наша цель — натренировать алгоритм, а не закопаться в редких тонкостях. Когда алгоритм станет привычкой, сложные случаи тоже перестанут пугать.

Первые три кейса — это ветка wrong winner: effective value уже не то, и мы ищем более сильный источник. Кейс с баннером другой: значение может быть правильным, а проблема всё равно в timing.

app.catalog.title не меняется из-за CLI

Самый частый сюжет выглядит так: вы меняете src/main/resources/application.yaml, ожидаете увидеть новый заголовок на landing page, но он упорно остаётся старым. Первое желание — переписать YAML ещё раз «на всякий случай». Но правильное действие — спросить Environment, а затем проверить запуск.

Предположим, у вас в packaged YAML стоит:

app:
  catalog:
    title: "File title"

А запуск в IDE (неочевидно для вас) содержит Program arguments:

--app.catalog.title=CLI title

Итог вполне предсказуем: CLI title победит, потому что command-line args сильнее packaged config. Ваш PropertyTraceRunner честно покажет:

app.catalog.title=CLI title

Это не «YAML не работает». Это «YAML работает, но проиграл». Исправление тоже простое и инженерное: убрать аргумент из конфигурации запуска или поменять его на ожидаемое значение. После этого YAML снова станет источником истины по умолчанию.

max-featured-count переопределён через JSON

Этот кейс особенно коварный, потому что SPRING_APPLICATION_JSON часто выглядит как «какая-то магия» и прячется внутри одной переменной. Допустим, вы в YAML поменяли лимит:

app:
  catalog:
    max-featured-count: 4

Но при запуске у вас задано:

SPRING_APPLICATION_JSON='{"app":{"catalog":{"max-featured-count":2}}}'

В результате вы продолжаете видеть лимит 2 и начинаете думать, что YAML игнорируется. Хотя на самом деле всё наоборот: YAML читается, но JSON-источник находится выше по precedence.

Здесь особенно полезен PropertySourcePeekRunner: он покажет, что ключ сидит в каком-то JSON property source. После этого вы перестаёте «охотиться» по файлам и идёте ровно в то место, которое победило.

server.port внезапно не 8080: привет, SERVER_PORT

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

Представим, что в YAML у вас:

server:
  port: 8080

Но в окружении (терминал, IDE, система) задано:

SERVER_PORT=9090

Итог: приложение стартует на 9090. А вы смотрите в YAML и говорите: «Но там же 8080!» — да, там 8080, но YAML проиграл env var.

Здесь важно помнить ещё одну ловушку: иногда переменные окружения задаёт не вы, а инструменты вокруг. IDE может сохранять env vars в Run Configuration. Некоторые дев-окружения задают SERVER_PORT автоматически. Поэтому “проверить окружение” — это не паранойя, а часть нормальной диагностики.

Если вы печатаете environment.getProperty("server.port"), вы увидите итог (например, 9090). А если вы проверите “лестницу” источников, вы почти наверняка найдёте SERVER_PORT как победителя и сможете решить, хотите ли вы это override оставлять или убрать.

Баннер и timing

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

Типичный корректный конфиг:

spring:
  main:
    # Баннер печатается на самом раннем этапе старта, поэтому любое override нужно проверять по запуску.
    banner-mode: "off"

Если баннер всё равно печатается, действуйте тем же алгоритмом: проверьте Environment (что видит приложение), затем проверьте команду запуска (нет ли --spring.main.banner-mode=console или чего-то подобного), и убедитесь, что вы реально стартовали новый процесс. Timing тут не про “сложно”, а про “один раз на старте”: поздно проверять эффект, если старт уже прошёл.

8. Типичные ошибки при разборе precedence-конфликтов

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

Ошибка №1: искать проблему в YAML, не зафиксировав точный ключ.
Если вы путаете app.catalog.title и app.catalog.titles, или смотрите на maintenance-mode, а переопределяете maintenanceMode, вы можете часами «чинить» то, что не сломано. Первое действие — назвать ключ точно и одинаково в голове, в коде и в конфиге.

Ошибка №2: доверять «я точно так запускал» больше, чем фактической команде запуска.
IDE и терминал умеют хранить старые аргументы и переменные окружения так, будто это семейная реликвия. Если поведение неожиданное, смотрите Run Configuration и реальные args/env. Precedence почти всегда выигрывает тот источник, который вы забыли, а не тот, который вы помните.

Ошибка №3: путать «значение не задано» и «значение задано, но проиграло».
В первом случае вы лечите проблему добавлением значения (или исправлением имени). Во втором — вы лечите проблему удалением/изменением override в более сильном источнике. Пока вы не отличили эти сценарии, любая попытка “поправить YAML” может быть стрельбой в туман.

Ошибка №4: воспринимать placeholder с default как «главный источник».
${APP_CATALOG_TITLE:Spring+ Catalog} — это удобный fallback, но он не делает YAML сильнее. Если сверху прилетел CLI arg или SPRING_APPLICATION_JSON, default останется просто запасным вариантом, а не победителем. Это нужно принять как правило игры, иначе будет вечное ощущение «почему дефолт не победил».

Ошибка №5: пытаться «убрать значение» через null в SPRING_APPLICATION_JSON.
Интуиция подсказывает, что null должен стирать нижние уровни, но на практике это чаще означает «не задавать override». Если вы хотите понять, почему “не затёрлось” — сначала проверьте итоговое значение и победивший источник, и только потом решайте, как именно задавать отсутствие значения.

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