🍃 Розділ 5 · Питання #23

Що робить анотація @ComponentScan

У Spring Boot ця анотація автоматично входить до складу мета-анотації @SpringBootApplication.


🟢 Junior Level

@ComponentScan — це анотація Spring, яка вказує IoC-контейнеру, в яких пакетах (packages) необхідно шукати класи, позначені стереотипними анотаціями (@Component, @Service, @Repository, @Controller, @Configuration), щоб автоматично зареєструвати їх як біни в ApplicationContext.

У Spring Boot ця анотація автоматично входить до складу мета-анотації @SpringBootApplication.

package com.example.shop;

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

// Автоматично запускає сканування пакета com.example.shop та всіх його підпакетів:
// com.example.shop.service, com.example.shop.controller, com.example.shop.repository
@SpringBootApplication 
public class ShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(ShopApplication.class, args);
    }
}

Ручне налаштування

Якщо ваші сервіси або спільні бібліотеки знаходяться за межами кореневого пакета додатка, сканування налаштовується вручну:

@Configuration
@ComponentScan(basePackages = {"com.example.shop", "com.example.common"})
public class CustomScanConfig { }

🟡 Middle Level

Параметри анотації @ComponentScan

Анотація надає багатий набір параметрів для гнучкої фільтрації компонентів:

@Configuration
@ComponentScan(
    // Рядкове зазначення пакетів
    basePackages = "com.example.service",
    
    // Типобезпечне зазначення пакетів через маркерні класи (Best Practice)
    basePackageClasses = {ServiceMarker.class, RepositoryMarker.class},
    
    // Виключення класів за типом або анотацією
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE, 
        classes = LegacyBillingService.class
    ),
    
    // Додавання кастомних анотацій у сканування
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION, 
        classes = CustomComponent.class
    ),
    
    // Вимкнення дефолтних стереотипів (@Component, @Service тощо)
    useDefaultFilters = true
)
public class AdvancedScanConfig { }

Типи фільтрів (FilterType)

Spring підтримує 5 стратегій фільтрації кандидатів у біни:

  1. FilterType.ANNOTATION — перевірка наявності анотації над класом.
  2. FilterType.ASSIGNABLE_TYPE — перевірка успадкування або реалізації зазначеного класу/інтерфейсу.
  3. FilterType.REGEX — зіставлення імені класу з регулярним виразом (наприклад, .*Stub.*).
  4. FilterType.ASPECTJ — вибір за AspectJ-виразом типу.
  5. FilterType.CUSTOM — реалізація користувацького інтерфейсу org.springframework.core.type.filter.TypeFilter.

Чому basePackageClasses надійніше за basePackages

Зазначення базових пакетів рядками (basePackages = "com.example.service") загрожує прихованими багами:

  • Під час рефакторингу та перейменування пакетів в IDE рядкові літерали не оновлюються автоматично.
  • Помилка в назві пакета виявиться лише під час старту додатка (NoSuchBeanDefinitionException).

Використання basePackageClasses спирається на посилання на клас (зазвичай порожній інтерфейс-маркер):

@ComponentScan(basePackageClasses = OrderServiceMarker.class)

Під час рефакторингу IDE автоматично перенесе імпорт класу, а будь-яка друкарська помилка призведе до помилки компіляції ще до запуску тестів.


🔴 Senior Level

Внутрішній механізм: як працює ASM і чому класи НЕ завантажуються в JVM

Багато розробників помилково вважають, що Spring знаходить біни за допомогою звичайної Java-рефлексії (Class.forName()).

Чому рефлексія не використовується:

  1. Завантаження сотень сторонніх .class файлів у пам’ять JVM ClassLoader-ом засмічує область пам’яті Metaspace.
  2. Під час завантаження класу JVM зобов’язана виконати статичні блоки ініціалізації (static { ... }). Якщо в статичному блоці є звернення до бази даних або файлу, додаток впаде задовго до готовності контексту.

Як сканує Spring (ClassPathBeanDefinitionScanner):

  1. На етапі invokeBeanFactoryPostProcessors запускається ConfigurationClassPostProcessor.
  2. Сканер перетворює ім’я пакета на шлях пошуку ресурсів: classpath*:com/example/**/*.class.
  3. Для кожного знайденого файлу викликається фабрика SimpleMetadataReaderFactory.
  4. За допомогою бібліотеки ASM Spring зчитує байткод .class файлу як масив байтів та аналізує заголовки константного пулу (Constant Pool).
  5. Витягуються метадані: ім’я класу, суперклас, інтерфейси та наявність анотацій, у яких RetentionPolicy = RUNTIME.
  6. Формується легковажний дескриптор ScannedGenericBeanDefinition.
  7. Клас завантажується в JVM ClassLoader тільки в самий останній момент — коли викликається createBeanInstance() для створення об’єкта в купі!

Оптимізація холодного старту: spring-context-indexer

У великих мікросервісах із тисячами класів рекурсивний обхід файлової системи та сканування JAR-архівів через ASM може займати від 5 до 15 секунд під час кожного старту.

Рішення: Попередня індексація на етапі збирання

Додайте в pom.xml анотаційний процесор:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
    <optional>true</optional>
</dependency>
Під час компіляції (javac):
    spring-context-indexer аналізує проєкт і генерує маніфест:
    META-INF/spring.components
    
    Вміст файлу:
    com.example.service.UserService=org.springframework.stereotype.Service
    com.example.repository.OrderRepository=org.springframework.stereotype.Repository

Під час старту додатка:
    ClassPathScanningCandidateComponentProvider виявляє CandidateComponentsIndex.
    Замість сканування файлової системи Spring миттєво зчитує готовий список!
    Час старту скорочується в 2-4 рази.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

@ComponentScan задає базові пакети для автоматичного виявлення компонентів Spring (@Component, @Service, @Repository, @Controller, @Configuration). Входить до складу @SpringBootApplication, скануючи за замовчуванням поточний пакет та всі його дочірні директорії.

  • Під капотом керується компонентом ClassPathBeanDefinitionScanner.
  • Ключова особливість: сканування виконується за допомогою низькорівневого аналізу байткоду через бібліотеку ASM, без фактичного завантаження класів у пам’ять JVM і без виконання статичних блоків.
  • Для безпечного рефакторингу рекомендується використовувати basePackageClasses, а для прискорення старту великих проєктів — spring-context-indexer.

4 каверзних питання з відповідями

  1. Чи завантажує Spring класи в пам’ять JVM ClassLoader під час роботи @ComponentScan? Відповідь: Ні, не завантажує. Spring використовує бібліотеку ASM (MetadataReader) для читання байткоду скомпільованих файлів .class як потоку байтів. Це дозволяє прочитати анотації та сигнатури типів без завантаження класів у Metaspace і запобігає небажаному виконанню статичних блоків ініціалізації (static {}). Клас завантажується ClassLoader-ом лише на етапі безпосереднього інстанціювання біна.

  2. Чим basePackageClasses кращий за параметр basePackages? Відповідь: basePackages приймає рядкові літерали пакетів. При перейменуванні пакетів через IDE рядкові значення легко пропустити, що призведе до помилок у рантаймі. basePackageClasses приймає посилання на Java-класи (.class), забезпечуючи сувору перевірку типів під час компіляції та автоматичний рефакторинг.

  3. Що робить анотаційний процесор spring-context-indexer? Відповідь: Під час компіляції проєкту він сканує анотації та генерує статичний індексний файл META-INF/spring.components. При старті додатка Spring зчитує цей файл напряму, повністю вимикаючи повільне сканування файлової системи та JAR-архівів через ASM, що радикально прискорює холодний старт.

  4. Як за допомогою @ComponentScan вимкнути реєстрацію всіх стандартних @Component і реєструвати лише свої анотації? Відповідь: Виставити прапорець useDefaultFilters = false та задати потрібні анотації в параметрі includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyAnnotation.class).

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «@ComponentScan використовує стандартну Java Reflection для пошуку файлів на диску» — рефлексія не вміє шукати класи в пакетах; пошук виконується через ResourcePatternResolver та ASM.
  • ❌ «Якщо клас знаходиться у відсканованому пакеті, його static-блок виконається під час старту» — static-блок виконується лише під час ініціалізації класу ClassLoader-ом, а не під час ASM-сканування.
  • ❌ «@ComponentScan необхідно завжди явно ставити над кожним сервісом» — анотація встановлюється один раз над головним конфігураційним класом (або успадковується з @SpringBootApplication).
  • ❌ «spring-context-indexer сканує біни під час виконання HTTP-запитів» — це процесор компіляції, що генерує статичний індекс заздалегідь.

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