1. Время и таймлайн старта
Если precedence — это правила «кто победил» среди источников, то timing (время чтения) — это правила «успел ли победитель выйти на поле». В Spring Boot есть настройки, которые влияют на поведение приложения на самых ранних этапах старта, когда у вас ещё нет ни ваших @Component, ни ваших @Service, а иногда даже привычных логов ещё нет в нормальном виде.
Представьте, что вы пришли на вокзал и победили в споре «какой поезд самый важный». Поздравляю, но если поезд уже уехал, ваш аргумент не изменит реальность. С ранними свойствами ровно так: вы можете установить значение поздно, вы даже сможете увидеть его через Environment, но эффект “на старте” уже не произойдёт. Поэтому сегодня нам нужно добавить к модели precedence ещё одну ось — ось времени.
До этого вопрос был один: какое значение победило. Здесь добавляется второй: успело ли оно вообще подействовать до того, как ранний этап старта уже прошёл.
Таймлайн старта: logging.* и spring.main.*
Чтобы не пытаться запоминать «какие-то магические исключения», лучше один раз увидеть упрощённый таймлайн старта. В нём не будет всех внутренних деталей Boot (мы не пишем диссертацию по SpringApplication), но будет достаточно, чтобы предсказывать поведение ранних свойств и не гадать по консоли как по кофейной гуще.
Ниже — практичная схема старта приложения уровня курса. Слева — этап, справа — что примерно происходит и где оказываются «ранние» свойства:
# Важно: это не «внутренности Spring Boot по шагам», а учебная карта,
# чтобы понимать, на каком этапе какие свойства вообще успевают сработать.
main()
↓
SpringApplication / подготовка
↓ (тут формируется ранний Environment)
configure logging ←───── читает logging.*
↓
print banner ←───── читает spring.main.banner-mode
↓
create ApplicationContext
↓
refresh context (bean creation)
↓
run ApplicationRunner / CommandLineRunner
↓
приложение "живое" (web-сервер поднят, контроллеры работают)
Для удобства можно свести это в таблицу:
| Этап старта | Что происходит “по-человечески” | Примеры свойств, которые важны здесь |
|---|---|---|
| Подготовка окружения | Boot собирает доступные источники свойств и строит Environment | базовые server.*, spring.*, app.* |
| Раннее логирование | Настраивается logging system, чтобы логи вообще были логами, а не хаосом | logging.level.*, часть logging.* |
| Баннер и поведение запуска | Печатается баннер, выбираются некоторые режимы старта | spring.main.banner-mode, некоторые spring.main.* |
| Контекст и бины | Создаются бины, поднимается web-слой | уже не “раннее” |
| Runner’ы | Запускается ваш код в ApplicationRunner и т.д. | уже поздно влиять на ранние этапы |
Ключевой вывод: ApplicationRunner — это после печати баннера и после настройки раннего логирования. Поэтому любые попытки «включить DEBUG на старте» из runner’а будут звучать как «давайте включим фары на машине… после того как мы уже приехали».
2. Ранние свойства logging.*
С логированием у новичков часто сложные отношения: сначала кажется, что это “просто System.out.println на стероидах”, а потом выясняется, что именно по логам вы понимаете, что вообще происходит в сервисе. Но есть ещё один нюанс: чтобы логи нормально работали, Boot должен настроить logging system очень рано — иначе он не сможет красиво логировать даже собственный старт.
В рамках этой лекции нам не нужно превращаться в специалистов по Logback или спорить о судьбах мира на уровне Appender-ов. Нам важно другое: свойства logging.* относятся к тем, которые Boot применяет раньше, чем большинство вашего кода успевает хотя бы моргнуть. Поэтому если вы хотите видеть, например, DEBUG-сообщения во время старта, их нельзя “добавить позже” — их нужно задать так, чтобы Boot увидел значение заранее.
Минимальная настройка уровней в YAML
Начнём с самого бытового: уровни логирования. Уровни — это просто “сколько подробностей печатать”. Для курса достаточно помнить, что INFO обычно спокойный режим, а DEBUG — подробный (иногда слишком).
Пример для catalog-service, где мы хотим оставить корневой уровень INFO, но свой пакет сделать более разговорчивым:
logging:
level:
# Корневой уровень: по умолчанию для всего приложения
root: INFO
# Точечное повышение детализации для своего пакета
com.example.catalogservice: DEBUG
Здесь важно не то, что именно вы выбрали, а то, что это свойство Boot подхватит очень рано, и часть стартовых сообщений уже будет отфильтрована/показана в нужной детализации.
Ранний override через command-line args
Иногда вам нужно временно “усилить слух” прямо на запуске, не трогая файлы. Тогда вы делаете override через аргументы:
# Временный override прямо при запуске через Gradle (без правки YAML)
./gradlew bootRun --args="--logging.level.root=DEBUG"
Или в более общем виде:
# То же самое, но при запуске уже собранного jar
java -jar app.jar --logging.level.root=DEBUG
Это как включить “режим детектива” ещё до того, как приложение начало рассказывать вам историю своей жизни. С точки зрения timing всё идеально: аргументы командной строки доступны Boot сразу, и logging system настраивается с их учётом.
Ограничения позднего DEBUG
Тут важно поймать одну простую мысль. Если вы включили DEBUG после старта, то вы, возможно, получите DEBUG-логи после включения. Но вы не получите DEBUG-логи прошлого. Логи за старт уже “пролетели”. Поэтому если ваша цель — диагностировать, почему приложение не стартует или стартует странно, logging.* должен быть задан так, чтобы сработать именно на раннем этапе.
3. Ранние свойства spring.main.*
Если logging.* — это “как именно мы рассказываем историю старта”, то spring.main.* — это “какую историю старта мы вообще проживаем”. Это группа свойств, которые управляют поведением SpringApplication и базовыми решениями о запуске. И, как нетрудно догадаться, многие из этих решений принимаются очень рано.
Самый понятный и почти мемный пример — баннер Spring Boot. Его видели все: тот самый ASCII-art, который радостно появляется в консоли, как кот, который считает себя главным в квартире. Баннер печатается до того, как начнёт работать ваш прикладной код, поэтому выключать его нужно “до печати”, а не “после”.
spring.main.banner-mode: выключаем баннер “вовремя”
В YAML это выглядит так:
spring:
main:
# Баннер печатается очень рано, поэтому значение должно быть доступно на старте
banner-mode: "off"
Через command-line override:
# Подходит для разового запуска, когда не хочется править конфиг
java -jar app.jar --spring.main.banner-mode=off
Обратите внимание на маленькую, но важную деталь: в YAML у нас "off" в кавычках. Это не обязаловка, но так новичкам проще: меньше шансов случайно превратить значение во что-то странное (YAML любит иногда быть “слишком умным”).
Ещё один пример: логировать ли информацию о старте
Есть свойство, которое часто путают с логированием уровня logging.*, хотя оно про другое: “логировать ли стартовую информацию вообще”. Например, spring.main.log-startup-info влияет на то, будет ли Boot печатать некоторые сообщения о старте.
Пример:
spring:
main:
# Не про уровень логов, а про то, печатать ли часть стартовых сообщений вообще
log-startup-info: true
Это свойство — хороший пример того, что spring.main.* — это не “настройки вашего домена”, а именно настройки механики старта приложения. И, соответственно, их применение происходит очень рано.
Ограничения изменений spring.main.* после старта
Если свойство влияет на то, как запускаться, то после запуска оно уже не может “переиграть прошлое”. Это как пытаться заменить фундамент дома после того, как вы уже поставили на него стены и повесили шторы. Поэтому spring.main.* нужно задавать так, чтобы Boot увидел его до SpringApplication.run(...) и успел применить.
4. Значение есть, эффекта нет
Сейчас будет самый полезный момент лекции: мы намеренно создадим ситуацию, когда значение свойства “вроде бы есть”, но поведение уже не меняется. Это помогает перестать путать две вещи: “конфигурация как данные” и “конфигурация как влияние на конкретный этап жизненного цикла”.
Представим два варианта: правильный (ранний) и поздний.
Ранний вариант: задали свойство до run(...)
Да, это хардкод. Да, обычно так делать не нужно. Но как демонстрация timing — идеально: мы “вносим настройку” до старта и видим, что она успевает повлиять.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Важно: задаём свойство ДО run(...), чтобы Boot успел применить его на ранней стадии
System.setProperty("spring.main.banner-mode", "off");
// Здесь начинается реальный старт приложения
SpringApplication.run(CatalogServiceApplication.class, args);
}
}
Смысл примера не в том, чтобы вы теперь всегда писали System.setProperty в main(). Смысл в том, что баннер — раннее поведение, и оно управляется значением, которое должно быть доступно до run(...).
Поздний вариант: пытаемся “выключить баннер” из ApplicationRunner
А теперь сделаем как часто делает новичок: “ну у меня же есть Environment… ну у меня же есть runner… сейчас всё настрою”.
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
class LateBannerSwitchRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// Ставим свойство слишком поздно: баннер уже напечатан к этому моменту
System.setProperty("spring.main.banner-mode", "off");
// Показать значение мы можем...
System.out.println("banner-mode=" + System.getProperty("spring.main.banner-mode"));
// banner-mode=off
// ...но «прошлое поведение» (печать баннера) этим уже не отменить.
}
}
Вы увидите banner-mode=off. То есть значение “поставилось”. Но баннер к этому моменту уже был напечатан. И вот это как раз тот случай, когда Environment (или System.getProperty) говорит вам правду про текущее значение, но эта правда не означает, что прошлое поведение изменилось.
Та же идея на примере логирования: “включили DEBUG поздно”
Если вы включаете logging.level.root=DEBUG слишком поздно, вы не заставите приложение “перелогировать” то, что уже произошло. Можно даже вывести это как мини-логический парадокс: “почему мой DEBUG не показывает старые события?” — потому что у логов нет машины времени.
Как мягкую иллюстрацию можно добавить runner, который пытается поздно переключить уровень и пишет DEBUG-сообщение:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
class LateDebugRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(LateDebugRunner.class);
@Override
public void run(org.springframework.boot.ApplicationArguments args) {
// Пытаемся «включить DEBUG» после старта: это не вернёт DEBUG-логи запуска
System.setProperty("logging.level.root", "DEBUG");
// Это сообщение может появиться уже после старта, но стартовые события не «перепишутся»
log.debug("Это DEBUG после старта (на старте всё равно было INFO).");
}
}
Даже если конкретное поведение логгера может зависеть от того, как logging system реагирует на изменения system properties, идея остаётся: ранние логи старта вы этим точно не получите. И это главная практическая мысль сегодняшней лекции.
5. Источники и проверка ранних свойств
Теперь соберём это в понятное правило. Если свойство должно влиять на ранний этап, оно должно прийти из источника, который доступен Boot на раннем этапе “подготовки окружения”. В рамках дня мы рассматриваем вполне конкретный набор источников, и хорошая новость: они как раз и относятся к тем, что Boot видит рано.
Ниже — короткая, но прикладная таблица: что обычно “успевает” для logging.* и spring.main.* в нашем учебном мире.
| Источник | Пример | Успевает для ранних свойств? | Почему |
|---|---|---|---|
| Конфиг в application.yaml внутри приложения | src/main/resources/application.yaml | Да | Boot читает packaged config при старте |
| Внешний конфиг рядом с запуском | ./config/application.yaml | Обычно да | Boot подхватывает external config до запуска контекста |
| Environment variables | LOGGING_LEVEL_ROOT=DEBUG | Да | Env vars доступны ещё до запуска JVM-кода приложения |
| System properties JVM | -Dlogging.level.root=DEBUG | Да | JVM-параметры существуют до старта Boot |
| Command-line args Boot | --logging.level.root=DEBUG | Да | Аргументы доступны Boot при построении Environment |
| SPRING_APPLICATION_JSON | JSON-override нескольких ключей | Да | Boot превращает JSON в property source рано |
А вот что “часто не успевает” (мы не углубляемся в конкретные механики добавления источников, нам важен принцип): любые значения, которые вы пытаетесь “подмешать” уже после того, как SpringApplication.run(...) прошёл ранние стадии. То есть runner’ы, @PostConstruct, ручные присваивания переменных — это уже другой слой.
Проверка ранних свойств по поведению
Когда новичок начинает диагностировать конфигурацию, он почти всегда делает одно и то же: печатает environment.getProperty("что-то") и успокаивается. Это неплохой старт, но для ранних свойств проверка должна быть “поведенческой”: вы смотрите, как именно стартовало приложение, потому что именно это вы и пытались изменить.
Проверять ранние свойства удобно по двум сигналам. Для spring.main.banner-mode вы просто наблюдаете: баннер печатается или нет. Для logging.level.* вы наблюдаете, появляются ли на старте сообщения нужного уровня. Это простая мысль, но она экономит часы жизни: раннее свойство доказывается не “содержимым Environment после запуска”, а тем, что ранний этап реально прошёл иначе.
Если хочется подстраховаться на проекте catalog-service, можно держать в голове такой метод: задайте --spring.main.banner-mode=off как command-line arg и убедитесь, что консоль “стала тише” в самом начале (не из-за логов, а именно из-за отсутствия баннера). Затем задайте --logging.level.com.example.catalogservice=DEBUG и убедитесь, что ваши DEBUG-сообщения (если они есть) начали появляться. Главное — не путайте “свойство установлено” и “свойство применено на нужной стадии”.
Отсюда и рабочее правило: при странном старте мало просто вывести значение через Environment. Сначала проверяем, верное ли оно вообще, а затем спрашиваем, не опоздало ли оно для раннего поведения приложения.
6. Типичные ошибки при работе с ранними свойствами
Ошибка №1: поздняя настройка из ApplicationRunner.
ApplicationRunner запускается уже после того, как Boot сделал множество ранних действий: настроил логирование, напечатал баннер, создал и refresh’нул контекст. Если вы меняете ранние свойства там, вы меняете “послестартовую реальность”, а не историю запуска. Это нормально, просто это другой момент времени.
Ошибка №2: считать Environment после старта доказательством применения на старте.
Environment после старта показывает текущую эффективную конфигурацию, но он не обязан гарантировать, что конкретное значение было использовано именно на раннем этапе. Баннер — лучший пример: вы можете увидеть spring.main.banner-mode=off после запуска, но баннер уже давно напечатан, и назад его не “заберёшь”.
Ошибка №3: уход в logging.* как в тему логирования целиком.
В этой лекции logging.* нужен как пример ранних свойств и зависимости поведения от времени чтения. Если начать копать в appenders, шаблоны, форматы и файлы конфигурации логов, вы потеряете центральную идею: ранние настройки должны быть доступны Boot до того, как он начнёт активно логировать.
Ошибка №4: ожидать, что поздний источник “переиграет” раннее поведение.
Очень хочется думать, что если источник “сильнее по precedence”, то он всегда победит. Но если он подключился поздно, то он может победить только “в данных”, а не “в уже совершившемся поведении”. Для ранних свойств важно не только “кто сильнее”, но и “когда он вообще появился”.
Ошибка №5: путать DEBUG “на старте” и DEBUG “после старта”.
Это разные задачи. Если вам нужны подробности про старт, logging.level.* задаётся до запуска (YAML/env/CLI). Если вам нужно временно усилить логирование в уже живом приложении, это отдельная история и отдельные инструменты, но в рамках текущей темы главное — не ожидать от “позднего включения” того, что относится к раннему этапу.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ