🍃 Раздел 5 · Вопрос #21

Как работает @SpringBootApplication

Если заглянуть в объявление @SpringBootApplication, мы увидим:


🟢 Junior Level

@SpringBootApplication — это главная композитная мета-аннотация, с которой начинается любое приложение на Spring Boot. Она устанавливается над классом, содержащим метод main(), и объединяет три важнейшие функции фреймворка.

package com.example.shop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}

3 составные части аннотации

Если заглянуть в объявление @SpringBootApplication, мы увидим:

  1. @SpringBootConfiguration (специализированная версия @Configuration):
    • Объявляет класс источником определений бинов (@Bean). Позволяет инструментам тестирования (@SpringBootTest) автоматически находить главную конфигурацию приложения.
  2. @EnableAutoConfiguration:
    • Активирует «магию» Spring Boot — автоматическую настройку компонентов (БД, безопасности, веб-сервера, сериализатора JSON) на основе обнаруженных в classpath библиотек и настроек в application.yml.
  3. @ComponentScan:
    • Включает автоматическое сканирование директорий в поисках классов, помеченных аннотациями @Component, @Service, @Repository, @Controller, начиная с текущего пакета (com.example.shop) и во всех его дочерних подпакетах.

Главное правило размещения: Всегда размещайте класс с @SpringBootApplication в корневом пакете вашего проекта (например, com.company.project). Тогда все сервисы, контроллеры и репозитории в подпакетах будут гарантированно обнаружены.


🟡 Middle Level

Параметры аннотации @SpringBootApplication

Аннотация служит удобным фасадом и перенаправляет свои параметры во внутренние аннотации:

@SpringBootApplication(
    // Передаётся в @EnableAutoConfiguration: исключение ненужных автоконфигураций
    exclude = {DataSourceAutoConfiguration.class}, 
    
    // Передаётся в @ComponentScan: переопределение пакетов сканирования
    scanBasePackages = {"com.example.shop", "com.example.common"},
    
    // Типобезопасное указание пакетов через маркерные классы
    scanBasePackageClasses = {CommonMarker.class},
    
    // Передаётся в @SpringBootConfiguration: отключение CGLIB-прокси для конфигурации
    proxyBeanMethods = true
)
public class ShopApplication { }

Сквозной пайплайн выполнения: SpringApplication.run(...)

Когда вызывается метод SpringApplication.run(Application.class, args), происходит строгая последовательность системных шагов:

1. Определение типа приложения (SERVLET, REACTIVE или NONE)
             ↓
2. Подготовка Environment (Загрузка application.yml, переменных ОС, аргументов CLI)
             ↓
3. Создание ApplicationContext (например, AnnotationConfigServletWebServerApplicationContext)
             ↓
4. Подготовка контекста (Применение ApplicationContextInitializer, регистрация Application.class)
             ↓
5. context.refresh() (Запуск ядра Spring: сканирование, создание бинов, запуск Tomcat)
             ↓
6. Запуск Runner-бинов (CommandLineRunner / ApplicationRunner)
             ↓
7. Публикация события ApplicationReadyEvent (Приложение полностью готово к трафику)

Запуск встроенного веб-сервера: фаза onRefresh()

Один из самых популярных вопросов: «На каком именно этапе поднимается встроенный Tomcat/Jetty?»

Веб-сервер запускается внутри фазы context.refresh(), а именно в защищенном методе onRefresh() класса ServletWebServerApplicationContext:

  1. Контейнер находит фабрику веб-сервера (ServletWebServerFactory, по умолчанию TomcatServletWebServerFactory).
  2. Создается экземпляр WebServer (Tomcat), конфигурируются коннекторы и открывается сетевой порт (по умолчанию 8080).
  3. Это происходит до того, как создаются обычные бизнес-бины, но связывание DispatcherServlet завершается перед финальной публикацией готовности.

🔴 Senior Level

Фазы context.refresh() в деталях

Сердцем метода run() является вызов AbstractApplicationContext.refresh(). В контексте @SpringBootApplication критичны следующие шаги:

  1. invokeBeanFactoryPostProcessors(beanFactory):
    • Вызывается ConfigurationClassPostProcessor.
    • Парсится класс @SpringBootApplication.
    • Запускается @ComponentScan: через библиотеку ASM сканируются .class-файлы и создаются BeanDefinition для всех @Component.
    • Запускается AutoConfigurationImportSelector: считывается .imports-файл, проверяются условия @Conditional и регистрируются автоконфигурации.
  2. registerBeanPostProcessors(beanFactory):
    • Регистрируются все BPP (AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, AOP Proxy Creators).
  3. onRefresh():
    • Инициализируется и стартует встроенный WebServer (Tomcat).
  4. finishBeanFactoryInitialization(beanFactory):
    • Метод beanFactory.preInstantiateSingletons(): инстанцируются, наполняются зависимостями и инициализируются все синглтон-бины.
  5. finishRefresh():
    • Публикуется ContextRefreshedEvent.

Механизм FailureAnalyzer

Если на этапе старта контекста происходит ошибка (например, занят порт 8080 или не задан пароль к БД):

  • Вместо гигантского пугающего стек-трейса на 500 строк Spring Boot перехватывает исключение через SPI-интерфейс FailureAnalyzer.
  • Встроенные анализаторы (PortInUseFailureAnalyzer, NoSuchBeanDefinitionFailureAnalyzer) форматируют понятный отчет:
===========================
APPLICATION FAILED TO START
===========================

Description:
Web server failed to start. Port 8080 was already in use.

Action:
Identify and stop the process that is listening on port 8080 or configure this application to listen on another port.

Runner интерфейсы: CommandLineRunner vs ApplicationRunner

После того как контекст полностью поднят, Spring Boot выполняет все бины, реализующие один из runner-интерфейсов:

Интерфейс Метод Особенности работы с аргументами CLI
CommandLineRunner run(String... args) Передает сырой массив строк: ["--port=9090", "test"]
ApplicationRunner run(ApplicationArguments args) Предоставляет удобный парсинг аргументов: args.getOptionValues("port"), args.getNonOptionArgs()
@Component
public class StartupTasks implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        if (args.containsOption("seed-data")) {
            System.out.println("Загрузка тестовых данных в базу...");
        }
    }
}

🎯 Шпаргалка для интервью

30-секундный ответ

@SpringBootApplication — мета-аннотация, объединяющая:

  1. @SpringBootConfiguration: объявляет класс конфигурационным (с поддержкой @Bean) и служит маркером для тестов (@SpringBootTest).
  2. @EnableAutoConfiguration: запускает импорт автоконфигураций через AutoConfigurationImportSelector.
  3. @ComponentScan: рекурсивно ищет компоненты Spring начиная с текущего пакета.

Метод SpringApplication.run() готовит Environment, создает ApplicationContext, в фазе onRefresh() поднимает встроенный веб-сервер (Tomcat), создает все синглтоны и в конце запускает CommandLineRunner / ApplicationRunner.

4 каверзных вопроса с ответами

  1. Что произойдет, если объявить класс с @SpringBootApplication в дефолтном пакете (без строки package ...;)? Ответ: @ComponentScan без указания базового пакета начинает сканировать текущий пакет. Если класс находится в корне (default package), Spring попытается просканировать абсолютно весь classpath, включая все подключенные JAR-зависимости. Это приведет к колоссальному падению производительности при старте, переполнению памяти или ошибкам загрузки сторонних классов.

  2. На каком этапе запуска приложения стартует встроенный Tomcat? Ответ: Внутри метода context.refresh(), а именно в переопределенном методе onRefresh() класса ServletWebServerApplicationContext. На этом этапе уже готовы BeanFactoryPostProcessor, но синглтон-бины бизнес-логики еще не инстанцированы.

  3. В чём разница между CommandLineRunner и ApplicationRunner? Ответ: Оба интерфейса выполняются после завершения сборки контекста перед переходом в рабочий режим. Разница в сигнатуре: CommandLineRunner принимает сырой строковый массив аргументов (String... args), а ApplicationRunner принимает объект ApplicationArguments, умеющий удобно разделять именованные опции (--foo=bar) и позиционные аргументы.

  4. Зачем нужна аннотация @SpringBootConfiguration, если уже есть стандартная @Configuration? Ответ: @SpringBootConfiguration является мета-аннотацией над @Configuration. Ее главное предназначение — служить уникальным маркером конфигурации Spring Boot, который автоматически ищется слайс-тестами (@DataJpaTest, @WebMvcTest, @SpringBootTest) для обнаружения корневой конфигурации приложения.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «@SpringBootApplication просто заменяет метод main» — это композитная аннотация из конфигурации, компонент-сканирования и автоконфигурации.
  • ❌ «Встроенный Tomcat запускается до создания ApplicationContext» — Tomcat запускается внутри context.refresh() (фаза onRefresh).
  • ❌ «@SpringBootApplication сканирует все папки на жестком диске» — сканируется строго текущий Java-пакет класса и его подпакеты.
  • ❌ «CommandLineRunner запускается до инициализации бинов» — раннеры запускаются строго ПОСЛЕ того, как все синглтон-бины созданы и инициализированы.

Связанные темы