🍃 Раздел 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-запросов» — это процессор компиляции, генерирующий статический индекс заранее.

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