Почему Spring-приложение запускается медленно, и на каком именно этапе оно тупит?

В новом переводе от команды Spring АйО разбираемся, как заглянуть внутрь процесса создания контекста и отследить инициализацию бинов.

1. Введение

Запуск приложения способен повлиять на общую производительность во время нагрузки. Например, во время запуска возможно создание всех объектов, необходимых для первоначальной работы, а также кэширование или подготовку данных, которые с высокой вероятностью понадобятся. Тем не менее, тут не всё так очевидно.

В этом руководстве мы узнаем, как упростить, отслеживать и контролировать создание контекста Spring-приложения.

Сначала кратко вспомним, что такое контекст Spring и зачем отслеживать его создание. Затем рассмотрим три реализации, которые Spring Boot предоставляет для удобного мониторинга создания контекста. Наконец, разберём, как и зачем может потребоваться собственная логика отслеживания запуска.

2. Контекст Spring-приложения

Контекст приложения Spring Boot служит контейнером инверсии управления — Inversion of Control, или IoC. Этот контейнер управляет жизненным циклом Spring-приложения и обеспечивает внедрение зависимостей, управление Spring-бинами, событиями и конфигурацией.

Контекст создаётся во время запуска Spring-приложения. Простые приложения Spring обычно запускаются быстро, однако в сложных проектах этот процесс может занимать больше времени и даже зависать во время инициализации.

Существует несколько способов сократить время запуска. Чтобы лучше понять процесс запуска, Spring предоставляет возможность отслеживать создание контекста приложения.

3. Интерфейс ApplicationStartup

Spring предоставляет две реализации интерфейса ApplicationStartup:

  • DefaultApplicationStartup

  • FlightRecorderApplicationStartup

Реализация по умолчанию фактически не выполняет никаких действий, тогда как FlightRecorderApplicationStartup предназначена для использования с Java Flight Recorder - JFR.

Кроме того, Spring Boot включает реализацию BufferingApplicationStartup, которая записывает события запуска в память. Если мы хотим использовать реализацию, отличную от DefaultApplicationStartup, необходимо настроить Spring-приложение с нужной реализацией ApplicationStartup до его запуска.

4. BufferingApplicationStartup

Рассмотрим BufferingApplicationStartup — реализацию, которая сохраняет события запуска в памяти и предоставляет actuator-эндпоинт для удобного доступа к записанным событиям.

4.1. Настройка

Сначала необходимо зарегистрировать BufferingApplicationStartup в приложении Spring Boot:

@SpringBootApplication
public class ApplicationStartupTrackingApplication {

    public static void main(String[] args) {

        SpringApplication app = new SpringApplication(ApplicationStartupTrackingApplication.class);

        app.setApplicationStartup(new BufferingApplicationStartup(2048));
        app.run(args);
    }
}

Параметр конструктора — 2048 в этом примере — задаёт размер буфера, то есть максимальное количество событий, которые могут быть записаны.

4.2. Доступ через actuator-эндпоинт

Один из способов получить записанные события — воспользоваться actuator-эндпоинтом. Для этого необходимо включить startup-эндпоинт Actuator в свойствах приложения:

management.endpoints.web.exposure.include=startup

Предположим, что в приложении также есть Spring-бин:

@Service
public class SpecialService {
  
}

При обращении к Actuator можно увидеть запись, соответствующую этому сервисному бину:

curl localhost:8080/actuator/startup | jq
{
  "duration": "PT0.000754S",
  "endTime": "2026-07-19T18:31:27.493778Z",
  "startTime": "2026-07-19T18:31:27.493024Z",
  "startupStep": {
    "id": 107,
    "name": "spring.beans.instantiate",
    "parentId": 4,
    "tags": [
      {
        "key": "beanName",
        "value": "specialService"
      }
    ]
  }
}

Важно отметить, что actuator-эндпоинт работает только с BufferingApplicationStartup. Если настроить приложение с любой другой реализацией интерфейса ApplicationStartup, будет возвращено сообщение об ошибке:

curl localhost:8080/actuator/startup | jq
{
  "timestamp": "2026-07-19T14:37:45.989Z",
  "status": 404,
  "error": "Not Found",
  "path": "/actuator/startup"
}

Кроме того, actuator-эндпоинт можно вызывать с помощью GET- или POST-запроса. В обоих случаях ответ должен быть одинаковым. Однако GET возвращает снапшот временной шкалы, а POST возвращает снимок и дополнительно очищает буфер.

4.3. Доступ через контекст приложения

Помимо actuator-эндпоинта, можно программно получить доступ к этой самой временной шкале, предоставляемой BufferingApplicationStartup. Для этого необходимо обратиться к настроенному экземпляру BufferingApplicationStartup:

@Component
public class StartupTracker {

    private BufferingApplicationStartup startup;
    
    public StartupTracker(@Autowired ApplicationContext context) {
        startup = (BufferingApplicationStartup) (
            (ConfigurableApplicationContext) context).getApplicationStartup();
    }

    public List recorded() {

        List recordedEvents = new ArrayList<>();

        for(StartupTimeline.TimelineEvent event : startup.getBufferedTimeline().getEvents()) {
            for(StartupStep.Tag tag : event.getStartupStep().getTags()) {
                recordedEvents.add(
                    event.getStartupStep().getName() + "  " +
                        tag.getKey() + " " +
                        tag.getValue()
                );
            }
        }

        return recordedEvents;
    }
}

Таким образом, мы получаем доступ к более подробным данным о процессе запуска контекста.

4.4. Использование в тестах Spring Boot

Это позволяет анализировать временную шкалу, создавать собственный формат вывода, фильтровать определённые события, а также тестировать реализацию:

@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
public class ApplicationTest {

    @Autowired
    private StartupTracker startupTracker;

    @Test
    void givenTheApplicationStarts_whenRetrieveRecordedEvents_ThenContainsCustomBeans() {
        Assertions.assertThat(startupTracker.recorded()).contains(
            "spring.beans.instantiate  beanName specialService",
            "spring.beans.instantiate  beanName startupTracker"
        );
    }
}

Мы видим, что список событий включает два созданных нами бина: SpecialService и StartupTracker.

Комментарий от Михаила Поливаха

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

Если она будет выполняться лениво, то потенциально первый клиент (а если вспомнить про контеншен - то их может быть несколько) получит довольно высокую latency. 

Можно, конечно, прописать логику в EventListener/InitilizingBean/@PostContrust и т.д. Но это же надо проверить в тестах, что Spring действительно нашел и выполнил этот коллбек. И вот тут если добавит т.н. StartupStep - вы можете гарантировано в тестах отслеживать, что Spring действительно выполнил нужный вам коллбек во время поднятия контекста. 

Опять же, я не говорю, что тяжелую логику нужно обязательно на старт контекста переносить, тут есть свои нюансы, но просто делюсь опытом.

Важно, что тест необходимо настроить с помощью SpringBootTest.UseMainMethod.ALWAYS. Это гарантирует запуск приложения через метод main, благодаря чему Spring Boot использует BufferingApplicationStartup, а не реализацию, не выполняющую никаких действий.

5. FlightRecorderApplicationStartup

Ещё один вариант отслеживания запуска приложения - FlightRecorderApplicationStartup. Эта реализация предоставляет метрики запуска Spring в формате, совместимом с Java Flight Recorder — JFR. Полученные данные можно просматривать с помощью Java Mission Control — JMC.

Настроим приложение на использование FlightRecorderApplicationStartup:

app.setApplicationStartup(new FlightRecorderApplicationStartup());

Кроме того, необходимо передать параметр JVM:

-XX:StartFlightRecording:filename=recording.jfr,duration=20s

После этого Spring будет записывать события запуска приложения в файл recording.jfr, который можно открыть в Java Mission Control - JMC.

В частности, этапы запуска можно найти в категории Spring Application.

6. Добавление пользовательских этапов

Рекомендуемый способ настройки временной шкалы запуска - добавить пользовательские этапы запуска в экземпляр ApplicationStartup, который можно получить из контекста приложения. Чтобы показать, как это сделать, создадим класс сервиса и добавим несколько пользовательских этапов:

@Service
public class SpecialService {

    private ApplicationStartup applicationStartup;

    public SpecialService(ApplicationContext context) {
        this.applicationStartup = ((ConfigurableApplicationContext) context).getApplicationStartup();
    }

    @PostConstruct
    public void init() {
        StartupStep startupStep1 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep1.tag("init", "connect to databases");
            // some long-running initialization
        } finally {
            startupStep1.end();
        }

        StartupStep startupStep2 = this.applicationStartup.start("com.baeldung.special.service");
        try {
            startupStep2.tag("init", "connect to AI agent");
            // more long-running initialization
        } finally {
            startupStep2.end();
        }
    }
}

Теперь в ответе Actuator можно увидеть этап инициализации бина SpecialService как родительский этап (id=107), а также два дополнительных этапа (id=[108,109]):

[
  {
    "duration": "PT0.000006S",
    "endTime": "2026-07-18T19:15:12.096227Z",
    "startTime": "2026-07-18T19:15:12.096221Z",
    "startupStep": {
      "id": 108,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to databases"
        }
      ]
    }
  },
  {
    "duration": "PT0.000002S",
    "endTime": "2026-07-18T19:15:12.096238Z",
    "startTime": "2026-07-18T19:15:12.096236Z",
    "startupStep": {
      "id": 109,
      "name": "com.baeldung.special.service",
      "parentId": 107,
      "tags": [
        {
          "key": "init",
          "value": "connect to AI agent"
        }
      ]
    }
  },
  {
    "duration": "PT0.000682S",
    "endTime": "2026-07-18T19:15:12.096328Z",
    "startTime": "2026-07-18T19:15:12.095646Z",
    "startupStep": {
      "id": 107,
      "name": "spring.beans.instantiate",
      "parentId": 4,
      "tags": [
        {
          "key": "beanName",
          "value": "specialService"
        }
      ]
    }
  }
]

В выводе представлены метрики для обоих этапов запуска: подключения к базам данных и подключения к ИИ-агенту, а также этап создания самого сервиса - spring.beans.instantiate. Как мы видели ранее, эти этапы запуска также можно отслеживать в Java Flight Recorder или программно через BufferingApplicationStartup.

7. Реализация интерфейса ApplicationStartup

Рекомендуемый способ доступа к метрикам запуска Spring - использование одной из трёх реализаций интерфейса ApplicationStartup, предоставляемых Spring. Однако можно создать и собственную реализацию:

public class CustomStartup implements ApplicationStartup {
    @Override
    public StartupStep start(String name) {
        return new CustomStartupStep(name);
    }
}

В этом случае также необходимо предоставить реализацию интерфейса StartupStep:

public class CustomStartupStep implements StartupStep {

    private String name;

    public CustomStartupStep(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    // Override other methods

}

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

8. Заключение

В этой статье мы рассмотрели, как Spring отслеживает создание контекста приложения с помощью интерфейса ApplicationStartup.

В частности, были рассмотрены три реализации, предоставляемые Spring:

  • DefaultApplicationStartup

  • BufferingApplicationStartup

  • FlightRecorderApplicationStartup

Во всех случаях мы узнали, как получать доступ к записанным этапам и добавлять дополнительные этапы во временную шкалу. Наконец, мы кратко рассмотрели, как подключить собственную реализацию интерфейса ApplicationStartup.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.