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" }
]
}
}
]
}
}
Чтобы не пытаться запомнить “всё сразу”, полезно держать маленькую шпаргалку, что здесь что:
| Поле | Что означает человеческими словами |
|---|---|
|
момент, когда начался измеряемый startup |
|
список измеренных событий (шагов) |
|
длительность в формате ISO‑8601 Duration ( = 0.150 секунды) |
|
имя шага (тип операции), часто начинается с spring.* |
|
id шага, чтобы отличать одно событие от другого |
|
родительский шаг (если есть иерархия) |
|
полезные “метки”, например имя бина (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 может быть вообще ни при чём. Он измеряет последовательность старта, а не производительность обработки запросов. Это нормально: у каждого инструмента своя зона ответственности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ