JavaRush /Курсы /Spring Boot /Startup visibility в Spring Boot

Startup visibility в Spring Boot

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

1. Startup timeline вместо одной строки в логах

Иногда кажется, что строка в логах вида Started CatalogServiceApplication in 2.3 seconds — это уже диагноз. Но это скорее «температура по больнице»: вроде понятно, что “быстро/небыстро”, но непонятно что именно было медленным. Endpoint /actuator/startup превращает старт в последовательность измеряемых шагов — и это сильно меняет качество разговора о проблеме.

Давайте честно: большинство проблем со стартом начинаются не с «всё сломалось», а с «вчера стартовало за 1 секунду, а сегодня за 8». И дальше начинается любимая игра начинающего разработчика: “я ничего не трогал(а)”. Spring Boot в этот момент тоже мог бы сказать: “я тоже ничего не трогал”, потому что чаще всего виноват не Boot, а ваши изменения в коде, конфиге или зависимостях.

Логи старта дают отличный общий обзор, но они не обязаны объяснять, где именно потрачено время. В реальном проекте у вас могут появиться тяжёлые участки: дорогая инициализация какого-то бина, чтение больших конфигов, сложная логика в @PostConstruct, «умный» ApplicationRunner, который делает половину работы до того, как приложение стало READY. И вот тут важно видеть старт как таймлайн.

У /actuator/startup есть простая суперсила: он позволяет не спорить “на ощущениях”, а смотреть на шаги старта как на факты. Это не профайлер JVM, не замена логов и не «таблетка от всех болезней», но это именно тот инструмент, который хорошо отвечает на вопрос: «а куда ушло время при старте, если раньше было быстрее?».

2. ApplicationStartup как встроенный секундомер

Чтобы endpoint startup вообще мог что-то показать, у Spring должен быть источник данных. Им является механизм ApplicationStartup. Если воспринимать это по-человечески, то это встроенный в фреймворк “журнал измерений”: Spring в разных местах старта помечает шаги, подписывает их, иногда добавляет теги (например, имя бина), и в итоге получается набор измерений, который можно анализировать.

Самое важное здесь — мысль, что Spring измеряет не «всё на свете», а конкретные шаги, которые он умеет инструментировать. Модель данных примерно такая: есть шаг (StartupStep) с именем (например, что-то про создание бина), у шага есть уникальный id, иногда есть parentId (чтобы построить иерархию), и есть набор тегов key/value, чтобы шаг был не абстрактным, а привязанным к реальности (например, beanName=courseCatalogSeeder).

Для новичка это особенно полезно, потому что вам не нужно знать весь Spring “наизусть”. Даже базовая способность увидеть: «вот шаг, вот имя, вот длительность, вот тег с именем бина» уже делает диагностику на порядок проще. Сама терминология тут тоже не страшная: ApplicationStartup — это интерфейс (контракт), а конкретная реализация определяет, куда эти измерения складывать.

Actuator endpoint /actuator/startup как раз и пытается показать эти измерения наружу. Но есть нюанс, и он принципиальный: если Spring ничего не собирал, показать нечего. Поэтому нам нужно включить такую реализацию ApplicationStartup, которая сохраняет шаги в памяти и делает их доступными для endpoint-а.

3. BufferingApplicationStartup: буфер и цена

BufferingApplicationStartup — это реализация ApplicationStartup, которая сохраняет собранные шаги старта в памяти (по сути, в буфере). Именно она и нужна, чтобы endpoint /actuator/startup мог вернуть понятный JSON. Логика здесь простая: сначала мы включаем сбор данных (иначе нечего показывать), затем Actuator предоставляет эти данные через endpoint.

Важно понимать, что слово “buffering” здесь не про «ускорение», а про «накопление». Это как поставить камеру на запись: вы не ускорили событие, вы просто теперь можете его пересмотреть. Но у камеры есть цена — память и небольшие накладные расходы на сбор данных. В большинстве учебных и локальных сценариев это нормально, но включать такую диагностику “везде и всегда” бездумно не стоит.

BufferingApplicationStartup создаётся с параметром размера, например new BufferingApplicationStartup(2048). В бытовых терминах это означает: “сколько шагов старта мы готовы хранить”. Если поставить слишком маленькое число, вы рискуете «обрезать» начало таймлайна или потерять детализацию. Если поставить очень большое — вы просто храните больше информации, чем вам реально нужно, и платите памятью.

Для нашего catalog-service (небольшого read-only сервиса) значение вроде 2048 — комфортный и понятный вариант. Он даёт достаточно места, чтобы увидеть, как поднимались основные части приложения: контекст, web-инфраструктура, конфигурация, ваши прикладные бины. При этом не превращает диагностику в отдельную подсистему, которой нужно заниматься как продуктом.

Ещё один важный момент: endpoint /actuator/startup прямо завязан на то, чтобы SpringApplication был сконфигурирован с BufferingApplicationStartup до вызова run(...). То есть «включить потом» уже не получится: как с видеозаписью старта футбольного матча — если вы включили запись после второго гола, первый вы не увидите, как ни старайтесь.

4. Включение сбора шагов в main()

Самое красивое в этой теме то, что настройка делается буквально в одном месте — в main(). Да, это тот редкий случай, когда “магия Spring” реально контролируется простой Java‑строчкой. Мы создаём объект SpringApplication, включаем BufferingApplicationStartup, и только потом запускаем приложение. До run() — это важно.

В нашем проекте catalog-service точка входа — CatalogServiceApplication. Если раньше у вас было классическое:

SpringApplication.run(CatalogServiceApplication.class, args);

то теперь нам нужно “развернуть” этот запуск, чтобы успеть вставить настройку:

package com.example.catalogservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;

@SpringBootApplication
public class CatalogServiceApplication {

    public static void main(String[] args) {
        // Создаём SpringApplication вручную, чтобы успеть включить сбор шагов старта ДО run()
        SpringApplication app = new SpringApplication(CatalogServiceApplication.class);

        // Включаем буферизацию шагов старта.
        // Важно: 2048 — это количество шагов, а не миллисекунды и не «время старта»
        app.setApplicationStartup(new BufferingApplicationStartup(2048));

        // Запускаем приложение уже с включённым сбором метрик старта
        app.run(args);
    }
}

Здесь важно уловить три идеи.

Первая идея: мы не меняем архитектуру приложения и не трогаем бины. Мы просто говорим Boot: «пожалуйста, собирай измерения старта».

Вторая идея: размер буфера (2048) — это не “время” и не “миллисекунды”, а количество шагов, которые мы готовы хранить. Это очень частая путаница.

Третья идея: если вы откатитесь обратно к SpringApplication.run(...), endpoint startup может перестать работать не потому, что вы “что-то сломали в Actuator”, а потому, что вы перестали собирать данные. То есть startup — это диагностический endpoint, который зависит от того, как вы запускаете приложение.

Если у вас включён DevTools и вы часто перезапускаете приложение, это тоже нормально. Каждый полноценный старт будет собирать свой набор шагов заново, и /actuator/startup будет показывать таймлайн именно последнего старта. Это удобно: вы можете менять код/конфиг и буквально наблюдать, как меняется картина старта.

5. Экспозиция /actuator/startup в конфигурации

Даже если данные собираются, Actuator не обязан показывать их наружу “просто так”. По умолчанию Spring Boot очень консервативен: наружу через HTTP экспонируется минимум endpoint-ов (обычно health), а остальное нужно явно включать через management.endpoints.web.exposure.include. И это хорошая привычка: диагностика должна быть управляемой, а не случайной.

Если шаги старта уже собираются, остаётся открыть endpoint в том же local-профиле, где живёт остальная диагностика. Здесь снова важна накопительная логика: мы добавляем startup к уже существующему списку, а не переписываем include под эту лекцию заново.

Минимальный пример:

management:
  endpoints:
    web:
      exposure:
        # Сохраняем уже открытые local/dev endpoints и добавляем startup
        include: "health,info,env,configprops,conditions,mappings,startup"

После этого endpoint станет доступен по пути:

GET /actuator/startup

Если вы видите 404 Not Found, не спешите ругать Actuator. В этой теме 404 чаще всего означает одну из двух вещей: либо endpoint не экспонирован (не включён в include), либо endpoint вообще не был создан, потому что приложение не собирает шаги старта (нет BufferingApplicationStartup в main()).

Ещё один тонкий момент: в JSON‑ответах Actuator иногда используются специфичные Content-Type (вроде application/vnd.spring-boot.actuator.v3+json). Вам как разработчику catalog-service обычно не нужно с этим бороться вручную — браузер и curl это “переварят”. Но если вы проверяете endpoint через какой-то клиент, и он капризничает, помните: Actuator — это API со своей семантикой и форматами.

6. Разбор JSON ответа /actuator/startup

Ответ /actuator/startup легко может напугать: там много времени, идентификаторов, вложенных объектов и каких-то странных PT0.00018S. На самом деле структура довольно регулярная. Если читать её как таймлайн событий, а не как «случайный JSON», всё быстро становится понятным: есть стартовое время, есть список измеренных событий, у каждого есть длительность и описание шага.

Упрощённый фрагмент ответа может выглядеть так (сильно укорочено, чтобы глаз не вытек):

{
  "springBootVersion": "4.0.3",
  "timeline": {
    "startTime": "2026-03-19T12:00:00.000Z",
    "events": [
      {
        "duration": "PT0.150S",
        "startupStep": {
          "name": "spring.boot.application.starting",
          "id": 0,
          "tags": [
            { "key": "mainApplicationClass", "value": "com.example.catalogservice.CatalogServiceApplication" }
          ]
        }
      },
      {
        "duration": "PT0.012S",
        "startupStep": {
          "name": "spring.beans.instantiate",
          "id": 12,
          "parentId": 5,
          "tags": [
            { "key": "beanName", "value": "courseCatalogController" }
          ]
        }
      }
    ]
  }
}

Чтобы не пытаться запомнить “всё сразу”, полезно держать маленькую шпаргалку, что здесь что:

Поле Что означает человеческими словами
timeline.startTime
момент, когда начался измеряемый startup
timeline.events[]
список измеренных событий (шагов)
events[].duration
длительность в формате ISO‑8601 Duration (
PT0.150S
= 0.150 секунды)
events[].startupStep.name
имя шага (тип операции), часто начинается с spring.*
events[].startupStep.id
id шага, чтобы отличать одно событие от другого
events[].startupStep.parentId
родительский шаг (если есть иерархия)
events[].startupStep.tags[]
полезные “метки”, например имя бина (beanName)

Особое внимание заслуживает формат длительности PT.... Он выглядит как заклинание, но это всего лишь стандарт ISO‑8601 для длительностей. PT0.150S — это 150 миллисекунд. Если будет, например, PT2.345S, это 2.345 секунды. Да, формат странноватый, но зато он однозначный и легко читается инструментами.

И ещё один важный психологический момент: не пытайтесь анализировать абсолютно каждый шаг. Вам почти никогда не нужно “прочитать всё”. Обычно вы ищете аномалии: шаги, которые занимают заметно больше времени, чем остальные, или повторяющиеся тяжёлые места.

7. Практика: поиск медленных мест на старте

Главная практическая польза startup‑таймлайна — не в том, чтобы “ускорить приложение любой ценой”, а в том, чтобы объяснить поведение и найти конкретную причину, когда старт стал неожиданно медленным. Это важное отличие: диагностика нужна, чтобы думать, а не чтобы паниковать.

Очень типичный сценарий: вы добавили в проект новый бин (например, какой-нибудь компонент в пакете catalog.bootstrap), и вдруг старт подрос. С /actuator/startup вы можете увидеть шаги spring.beans.instantiate с тегом beanName=.... Если один из таких шагов внезапно занимает, условно, 1–2 секунды, у вас появляется конкретная ниточка: “вот конкретный бин, который тяжело создаётся”. Дальше вы не перебираете всё приложение, а идёте прямо в этот класс и смотрите, что там происходит.

В учебном catalog-service это особенно показательно на таких компонентах, которые логично выполняют работу при старте. Например, если у вас есть сидер данных, который загружает курсы из конфигурации, и вы (случайно или намеренно) сделали эту загрузку тяжёлой в @PostConstruct, то бин начнёт “весить” старт. Вот пример структуры такого компонента (без лишней логики — нам важен именно жизненный цикл):

package com.example.catalogservice.catalog.bootstrap;

import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
class CourseCatalogSeeder {

    private static final Logger log = LoggerFactory.getLogger(CourseCatalogSeeder.class);

    @PostConstruct
    void seed() {
        log.info("Seeding catalog data...");
    }
}

@PostConstruct выполняется в момент инициализации бина, а значит, если внутри окажется что-то тяжёлое, это вполне может повлиять на таймлайн старта. И тогда /actuator/startup поможет увидеть: “ага, создание/инициализация этого бина занимает заметное время”.

Второй полезный подход — смотреть не на абсолютные цифры, а на сравнение. Если вчера старт был 1.2 секунды, а сегодня 3.8, и вы видите, что появился новый “длинный” шаг — это очень сильный сигнал. Но если старт был 1.2, стал 1.4, и вы видите несколько шагов чуть-чуть длиннее — это может быть шум, машина была занята, JIT по-другому разогрелся, да и в целом жизнь сложнее, чем идеально ровные миллисекунды.

Третий момент — границы применимости. Startup endpoint показывает то, что происходило во время старта приложения, а не в обычном runtime. Если у вас проблема «первые запросы после старта отвечают медленно», /actuator/startup может подсказать “общую картину”, но это не инструмент анализа производительности запросов. Он про старт, и это его сильная сторона.

И, наконец, приятный бонус: startup endpoint помогает разговору в команде. Вместо «мне кажется, стало медленнее» появляется “вот step spring.beans.instantiate для courseCatalogSeeder, длительность такая-то”. Это почти магическим образом превращает обсуждение в инженерное.

Snapshot и drain

Иногда полезно знать, что /actuator/startup может работать в двух режимах: получить “снимок” данных или “слить” (drain) данные из буфера. В большинстве случаев вам достаточно обычного чтения (snapshot), но сам факт “drain” помогает понять, что данные действительно живут в буфере, и с ним можно обращаться как с ресурсом.

В типичной модели Actuator API это выглядит так: GET возвращает снимок, а POST возвращает данные и очищает буфер. Смысл этого простой: если вы по какой-то причине хотите освободить память или не хотите, чтобы данные продолжали храниться, вы можете “слить” их один раз и дальше endpoint будет пустым, пока не будет нового старта.

Для курса достаточно помнить короткое правило: если вы просто хотите посмотреть, как стартовало приложение, используйте GET /actuator/startup. Если вы хотите забрать данные и очистить буфер (например, для разовой диагностики) — используйте POST /actuator/startup. В большинстве учебных сценариев это не критично, но понимание механики делает Actuator менее “магическим”.

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

Ошибка №1: экспонировать startup “на всякий случай” везде, включая prod.
Startup‑информация — это диагностика, и она может быть лишней в публичной среде. В нашем учебном проекте мы придерживаемся безопасной политики: расширенные endpoint’ы живут в local/dev, а не становятся случайной публичной поверхностью. Даже если кажется, что “там же ничего секретного”, это всё равно расширение attack surface и лишняя информация о внутренностях приложения.

Ошибка №2: включить endpoint в exposure.include, но забыть про BufferingApplicationStartup.
Это самая частая причина 404 или “пустоты”: вы открыли маршрут, но данных нет, потому что вы их не собираете. Endpoint startup — не “самодостаточный”: ему нужен источник измерений, и этот источник включается в main() до run().

Ошибка №3: поставить крошечный буфер и потом удивляться, что “таймлайн какой-то обрезанный”.
Если вы задали маленькое число шагов, Spring будет хранить меньше измерений. Для небольших приложений это может быть незаметно, но как только проект разрастётся, вы можете потерять детализацию. Для catalog-service разумный размер вроде 2048 — хороший баланс между “хватает данных” и “не делаем из диагностики отдельный продукт”.

Ошибка №4: пытаться “оптимизировать всё подряд” по одной цифре.
Startup timeline — это не список «виноватых», а карта. Увидеть шаг на 200 мс и сразу переписывать архитектуру — это типичный premature optimization. Гораздо здоровее использовать endpoint как указатель направления: где искать причину, если старт действительно стал проблемой.

Ошибка №5: ожидать, что /actuator/startup объяснит проблемы обычного runtime после старта.
Если приложение стартует за секунду, но потом первые запросы “тупят”, startup endpoint может быть вообще ни при чём. Он измеряет последовательность старта, а не производительность обработки запросов. Это нормально: у каждого инструмента своя зона ответственности.

1
Задача
Spring Boot, 23 уровень, 2 лекция
Недоступна
Endpoint `startup` с `BufferingApplicationStartup`
Endpoint `startup` с `BufferingApplicationStartup`
1
Задача
Spring Boot, 23 уровень, 2 лекция
Недоступна
Именованный startup-компонент в timeline
Именованный startup-компонент в timeline
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ