🍃 Розділ 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 запускається до ініціалізації бінів» — раннери запускаються строго ПІСЛЯ того, як усі синглтон-біни створені та ініціалізовані.

Пов’язані теми