JavaRush /Курсы /Spring Core /Связка @Component и ...

Связка @Component и @ComponentScan

Spring Core
5 уровень , 0 лекция
Открыта

1. Ручная регистрация бинов: когда надоедает

Если до сегодняшнего дня вы всё делали «правильно», ваш AppConfig или код запуска уже начал напоминать список покупок на неделю: вроде всё полезное, но растёт он быстрее, чем чувство контроля. Причина простая и вполне инженерная: контейнеру нужно сообщать, какие классы считать бинами. Вручную это работает, но удовольствие заканчивается, как только проект становится чуть больше «Hello, world».

Вспомните наш ContextFlow. Даже в учебной версии там довольно быстро появляются сервисы сценариев, хранилище заказов, аудит, уведомления, генерация id и так далее. Если каждый класс перечислять вручную, изменение в духе «добавил один класс» быстро превращается в «добавил класс, не забыл зарегистрировать, поправил импорт, не перепутал имена». Пока это ещё не катастрофа, но уже самое время посмотреть, как Spring обычно решает такую задачу в повседневных проектах.

Чтобы увидеть контраст, удобно держать в голове простую таблицу — она заменит длинные списки и сэкономит немного нервных клеток:

Вопрос Ручная регистрация Component scanning
Где «живёт» информация о регистрации? В конфигурации (часто далеко от класса) Прямо рядом с классом (аннотация на классе)
Что меняется при добавлении нового сервиса? Нужно не забыть «дописать в конфиг» Достаточно пометить класс и положить в правильный пакет
Риск «забыл зарегистрировать» Высокий Ниже (но появляются другие риски: границы сканирования)
Что делает контейнер? Регистрирует то, что вы явно перечислили Сам ищет кандидатов в заданной области

Сегодняшняя идея очень простая: мы не меняем Spring на другой Spring. Мы меняем только способ регистрации бинов: вместо «вручную перечислить» — «автоматически найти по правилам».

2. Component scanning: автоматизация регистрации

Термин «сканирование» звучит так, будто Spring берёт лазерный сканер, ходит по вашему проекту и читает мысли разработчика. К счастью или к сожалению, всё прозаичнее. Component scanning — это вполне прагматичный механизм: контейнер получает область поиска (обычно корневой пакет приложения) и ищет там классы, помеченные специальными аннотациями, чтобы зарегистрировать их как бины.

Важно сразу снять половину «магии»: scanning решает задачу регистрации, а не вопросы вроде «как внедрять зависимости», «как выбирать реализацию интерфейса» или «как жить с профилями». Сегодня нам нужна только минимальная рабочая модель:

пометили класс → задали область поиска → подняли контекст → получили bean

Вот и всё. Никаких тайных порталов в Boot и никакого «оно само как-то». Если класс не попал в область поиска, хоть обклейте его аннотациями — контейнер о нём не узнает. И наоборот: если область поиска задана слишком широко, контейнер может найти больше, чем вы ожидали, и вы снова пойдёте читать stack trace, только уже с новым сюжетом.

Есть и важная связь с прошлыми днями: найденный класс не превращается сразу в готовый объект. Сначала Spring находит его как кандидата, регистрирует BeanDefinition, а уже потом — в зависимости от стратегии старта — создаёт реальные экземпляры. Это тот же двухфазный подход, который мы уже обсуждали на Дне 4, просто источником BeanDefinition теперь становится scanning, а не ручной конфиг.

3. @Component: класс-кандидат в bean

Аннотация @Component — базовая метка, которой мы говорим Spring: «вот этот класс — кандидат на регистрацию как bean». Она находится в пакете org.springframework.stereotype и по смыслу максимально нейтральна: «компонент приложения». Позже мы увидим более узкие stereotype-аннотации, но сегодня держим фокус на базе.

Важно не перепутать два утверждения. Верное: «класс с @Component может стать bean-ом». Неверное: «класс с @Component уже стал bean-ом». Между ними есть ещё одна важная вещь — @ComponentScan, которая задаёт область поиска. Без сканирования @Component — это как наклейка «важно» на документе, который лежит в ящике, куда никто не заглядывает.

Минимальный пример на базе ContextFlow: пусть у нас есть порт AuditWriter, и первая реализация пишет аудит в консоль. Это отличный кандидат на компонент: инфраструктурный объект, который должен жить в контейнере.

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Component;

// Помечаем класс как компонент: при scanning Spring зарегистрирует его как bean
@Component
public class ConsoleAuditWriter implements AuditWriter {

    @Override
    public void write(String message) {
        // Простейшая реализация: пишем аудит в консоль (инфраструктурный код)
        System.out.println("[AUDIT] " + message); // [AUDIT] ...
    }
}

Обратите внимание на смысл этого куска кода: мы не создаём объект, не вызываем new и не трогаем контекст. Мы просто помечаем класс — то есть описываем намерение: «контейнер, пожалуйста, обрати на это внимание, если будешь сканировать».

И ещё один мягкий, но важный стоп-сигнал: не нужно делать bean-ом всё подряд. Доменные классы вроде Order, OrderItem, команды вроде CreateOrderCommand — это обычно обычные объекты, которые создаются по ходу сценария. Контейнер — не склад для всего подряд, а менеджер инфраструктуры и сервисов.

4. @ComponentScan: граница поиска

Если @Component — это метка на классе, то @ComponentScan — правило для контейнера: где искать такие классы. Чаще всего @ComponentScan ставят на конфигурационный класс — тот самый, с которого вы поднимаете ApplicationContext. И это логично: конфигурация — точка сборки приложения, а scanning — часть этой сборки.

Минимальная конфигурация для ContextFlow выглядит так: мы говорим Spring, что корневой пакет проекта — com.example.contextflow, и именно его нужно просканировать.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

// Класс конфигурации: через него мы отдаём Spring стартовые инструкции
@Configuration
// Граница scanning: Spring ищет компоненты внутри указанного пакета и подпакетов
@ComponentScan(basePackages = "com.example.contextflow")
public class AppConfig {
}

Здесь важно не застрять на слове @Configuration. Сегодня мы не разбираем «режимы» конфигурации и тонкости @Bean-методов. Нам достаточно простой мысли: это класс, который вы передаёте в AnnotationConfigApplicationContext, и через него Spring получает стартовые инструкции. Одна из них — «включи scanning».

После этого наш знакомый код запуска почти не меняется: мы всё так же создаём контекст, всё так же можем сделать getBean(), всё так же корректно закрываем контекст через try-with-resources.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {

    public static void main(String[] args) {
        // try-with-resources гарантирует корректное закрытие контекста
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Если контекст поднялся — значит конфиг прочитан и scanning сработал
            System.out.println("Context started"); // Context started
        }
    }
}

На этом месте у новичков часто возникает ожидание: «ну всё, я поставил @ComponentScan, значит Spring нашёл всё во Вселенной». Нет. Он нашёл всё внутри заданной границы, и только то, что подходит под критерии «компонента» (в нашем случае — аннотировано соответствующим образом).

5. Старт контекста: scanning → BeanDefinition → объект

Пока мы не зафиксируем цепочку «что за чем», scanning будет казаться магией. Но на самом деле он идеально ложится на уже знакомую модель из предыдущих дней: Spring любит сначала собрать метаданные, а потом создавать объекты. Component scanning просто становится одним из источников этих метаданных.

Представьте, что AppConfig — это «карта района», а @Component — «вывеска на доме». Когда контейнер стартует, он проходит примерно такой путь: получает карту, обходит район, видит вывески, составляет список домов и только потом начинает «вселять жильцов» — то есть создавать объекты и соединять их зависимостями.

Небольшая схема, которая обычно хорошо укладывается в голове (и уменьшает желание верить в фей):

flowchart TD
    A[AppConfig] --> B["@ComponentScan: basePackages"]
    B --> C[Classpath scanning: поиск классов]
    C --> D[Регистрация BeanDefinition]
    D --> E[Создание singleton-beans]
    E --> F[Готовый ApplicationContext]

Здесь самое важное — понять, что scanning не «создаёт объект». Он помогает контейнеру построить список того, что нужно создать. А дальше включаются уже знакомые механики: жизненный цикл, инъекция зависимостей и так далее. Сегодня мы не углубляемся в жизненный цикл, но фазы путать всё равно не стоит.

Если хочется увидеть это руками — а руками обычно понятнее, — можно сделать простую диагностику: попросить контекст сказать, зарегистрирован ли bean по имени. Это не «правильный бизнес-код», но для обучения и отладки — самое то.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Имя по умолчанию для ConsoleAuditWriter будет "consoleAuditWriter"
    System.out.println(context.containsBean("consoleAuditWriter")); // true
}

Смысл этого фрагмента не в containsBean(). Смысл в том, что после scanning у контекста действительно появляется запись о компоненте — и это уже не догадки, а факт, который можно проверить.

6. Имена bean-ов: правила и контроль

Как только вы включаете scanning, возникает практический вопрос: «Окей, контейнер нашёл класс. А как он его назвал?» И это не праздный интерес. Даже если вы обычно получаете бины по типу, имена всплывают в диагностике, ошибках, логах, а позже — в более сложных сценариях выбора кандидата.

По умолчанию Spring генерирует имя bean-а из имени класса, делая первую букву строчной. То есть ConsoleAuditWriter становится consoleAuditWriter. Выглядит логично и в большинстве случаев этого достаточно.

Если же имя важно, например вам нужно стабильное имя для получения bean-а в учебных целях или для будущего «явного выбора», его можно задать вручную прямо в @Component. Делается это через параметр value.

import org.springframework.stereotype.Component;

// Явно задаём имя bean-а (удобно, когда дальше делаем lookup по имени)
@Component("orderPrinter")
public class OrderPrinter {

    public String print(String orderId) {
        // Пример простого сервиса: возвращаем строку для печати заказа
        return "Order: " + orderId;
    }
}

Теперь вы можете получить его по имени:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Получаем bean по имени и типу (так безопаснее, чем только по имени)
    var printer = context.getBean("orderPrinter", OrderPrinter.class);
    System.out.println(printer.print("ORD-1")); // Order: ORD-1
}

Здесь важно не впасть в другую крайность: не нужно сразу везде задавать имена вручную «на всякий случай». В небольших приложениях и учебных примерах это иногда полезно, но в реальном коде чаще удобнее получать зависимости по типу. Наша цель на этом этапе — понимать, что имя у bean-а есть, откуда оно берётся и как сделать его предсказуемым, если это вдруг понадобится.

И да, небольшой факт на будущее — без подробностей: имена становятся особенно интересными, когда у вас появляется несколько реализаций одного интерфейса. Но сегодня мы туда не идём — сегодня мы учимся, чтобы потом не страдать.

7. Минимальный пример для ContextFlow

Чтобы закрепить механику, сделаем маленький, но законченный кусок. Не «перенесём всё приложение», а «сделаем минимальный шаг, который точно работает». В учебных проектах это почти всегда лучший способ не потеряться: двигаться маленькими проверяемыми шагами.

Для самой механики scanning нам сейчас не нужен полный рабочий сценарий ContextFlow. Хватит маленького ScenarioRunner без зависимостей: здесь важно увидеть не финальную сборку проекта, а сам факт, что контейнер нашёл класс и умеет вернуть его по типу.

import org.springframework.stereotype.Component;

// Компонент, который должен быть найден scanning-ом и зарегистрирован в контексте
@Component
public class ScenarioRunner {

    public void run() {
        // Демонстрационный эффект: видно, что bean реально создан и метод выполняется
        System.out.println("ContextFlow started"); // ContextFlow started
    }
}

Сканирование включено в AppConfig (мы его уже показали). Тогда запуск остаётся таким:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {

    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Получаем bean по типу: имя тут не важно, важно что он вообще в контексте
            var runner = context.getBean(ScenarioRunner.class);
            runner.run(); // ContextFlow started
        }
    }
}

И вот здесь — ключевая победа дня: ScenarioRunner попал в контекст не потому, что мы его явно зарегистрировали, а потому что контейнер нашёл его во время scanning. Именно ради этого всё и затевалось. Мы сокращаем конфиг-код и приближаем информацию о регистрации к самому классу.

Важно зафиксировать одну простую мысль: ApplicationContext остался тем же центром системы. Мы не «обошли Spring», мы просто научили контейнер автоматически находить часть бинов.

Проверка результата scanning

В реальной жизни после включения scanning самый частый вопрос звучит не философски, а очень прикладно: «Почему он не видит мой компонент?» Или, если день не задался: «Почему он увидел то, что я вообще не хотел видеть?» Поэтому на этом этапе полезно освоить пару диагностических приёмов, не превращая их в отдельный стиль программирования.

Один из самых простых способов — вывести список имён bean definition. Да, это выглядит шумно, но зато сразу видно: scanning вообще что-то зарегистрировал или нет. И вы можете быстро проверить, появилось ли имя вашего компонента.

import java.util.Arrays;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Диагностика: печатаем все зарегистрированные имена bean definition
    System.out.println(Arrays.toString(context.getBeanDefinitionNames())); // [..., scenarioRunner, ...]
}

Ещё один спокойный вариант — проверить наличие конкретного типа. Он часто приятнее, потому что вы мыслите классами, а не строками.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Быстрая проверка: если getBean не бросил исключение, значит компонент найден
    System.out.println(context.getBean(ScenarioRunner.class) != null); // true
}

Смысл этих проверок не в том, чтобы тащить их в production. Смысл в том, чтобы у вас была отвёртка для диагностики: если после добавления @Component и @ComponentScan класс не находится, вы сначала проверяете границу scanning и пакет, а не лезете в бизнес-логику и не начинаете подозревать Java в заговоре.

И короткая памятка: когда компонент не найден, чаще всего виноваты три вещи — нет @ComponentScan, неверный basePackages, или класс лежит не там, где вы думаете (например, после рефакторинга пакетов). Всё остальное случается реже и обычно приходит позже, когда сборка зависимостей становится сложнее.

8. Типичные ошибки при работе с @Component и @ComponentScan

На первом знакомстве component scanning кажется слишком простым: «ну что там, две аннотации». Именно поэтому ошибки оказываются особенно обидными: вам кажется, что всё сделано, а контейнер ведёт себя так, будто вашего кода не существует. Давайте разберём самые частые промахи не по названиям исключений, а по симптомам — так они запоминаются лучше.

Ошибка №1: поставить @Component, но забыть включить scanning.
Это классика. Вы пометили ScenarioRunner как @Component, но в конфигурации не появилось @ComponentScan (или конфигурационный класс вообще не используется при старте контекста). Симптом обычно простой: NoSuchBeanDefinitionException при попытке getBean(ScenarioRunner.class). Исправление тоже простое: убедиться, что контекст стартует именно с тем AppConfig, где есть @ComponentScan.

Ошибка №2: думать, что @Component «создаёт объект прямо сейчас».
Иногда ожидают, что как только вы написали @Component, где-то в JVM раздастся щелчок, и объект материализуется. На практике @Component лишь делает класс кандидатом, а реальное создание происходит на фазе создания экземпляров внутри старта контекста. Если это перепутать, появляется странная логика вроде «почему конструктор не вызывается при компиляции?». Спойлер: потому что компиляция не запускает контейнер.

Ошибка №3: помечать @Component всё подряд, включая доменные объекты.
Есть соблазн: «раз scanning — это удобно, пусть Spring управляет всем: Order, Customer, CreateOrderCommand…». Это быстро размывает архитектуру и превращает контейнер в сущность, которая знает слишком много. Доменным объектам обычно не нужна жизнь в контейнере: они создаются на каждую операцию, несут данные, и их проще контролировать обычным new. В контейнер обычно отправляют сервисы и инфраструктуру — то, что живёт долго и должно быть единым.

Ошибка №4: задать неверную границу scanning и потом лечить симптомы, а не причину.
Если basePackages слишком узкий, часть компонентов не найдётся. Если слишком широкий — может найтись лишнее, и тогда начнутся неожиданные конфликты. Новичок часто делает наоборот: видит ошибку создания какого-то сервиса и начинает править сервис, хотя проблема в том, что нужный компонент вообще не зарегистрирован. Правильная диагностика начинается с вопроса: «Класс точно попал в область поиска?»

Ошибка №5: полностью полагаться на имя bean-а по умолчанию, когда имя важно.
В учебных примерах вы иногда делаете lookup по имени, и тогда ошибка в имени превращается в потерю времени. Например, вы ожидаете "orderPrinter", а Spring сгенерировал другое имя или вы просто ошиблись в регистре. Если вы заранее знаете, что имя будет использоваться как часть контракта, например в демонстрации, задайте его явно через @Component("orderPrinter"). Это делает поведение предсказуемым и экономит нервы.

1
Задача
Spring Core, 5 уровень, 0 лекция
Недоступна
Минимальный scanning-контекст для приветствия
Минимальный scanning-контекст для приветствия
1
Задача
Spring Core, 5 уровень, 0 лекция
Недоступна
Явное имя bean-а для форматтера
Явное имя bean-а для форматтера
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ