JavaRush /Курсы /Spring Boot /Стереотипы и component scanning

Стереотипы и component scanning

Spring Boot
2 уровень , 2 лекция
Открыта

1. Stereotype-аннотации: контейнер без телепатии

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

Контейнеру нужен простой ответ на вопрос: какие классы считать «частями приложения», которыми нужно управлять? Если бы Spring пытался «сделать бином вообще всё», получилось бы странно. Во‑первых, не все классы имеют смысл как объекты долгой жизни. Во‑вторых, многие классы должны создаваться по ситуации — например, DTO-объекты или временные структуры. В‑третьих, легко получить конфликт: у вас есть класс Money или CourseCard, а Spring вдруг пытается собрать его как сервис. Это как назначить кружку «ответственной за бухгалтерию»: кружка, может, и хорошая, но роль явно не её.

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

Сразу аккуратно зафиксируем одну мысль: stereotype-аннотация не добавляет бизнес-логику. Она не делает класс умнее. Она лишь говорит: «контейнер, пожалуйста, обрати на меня внимание». Это как бейджик на конференции: бейджик не делает вас экспертом, но позволяет охране пустить вас внутрь.

2. @Component: минимальный маркер «это часть приложения»

В Spring есть базовая стереотип-аннотация — @Component. Она означает самое простое: «этот класс — компонент, зарегистрируй его как бин». Если вы не уверены, какой «профессии» ваш класс, но понимаете, что он должен жить в контейнере и участвовать в DI, @Component — нормальный старт.

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

Вот мини-пример в духе нашего будущего catalog-service: нормализатор slug’ов. Он маленький, но может использоваться в разных местах, и это как раз тот случай, когда приятно держать его в контейнере как переиспользуемый компонент.

package com.example.catalogservice.catalog.support;

import org.springframework.stereotype.Component;

@Component // Говорим Spring: этот класс нужно увидеть при сканировании и зарегистрировать как бин
public class SlugNormalizer {

    public String normalize(String slug) {
        // Пример «служебной» логики: не бизнес-правило, а утилитарная нормализация ввода
        return slug.trim().toLowerCase();
    }
}

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

Ещё один полезный момент: бин — это не «класс». Бин — это объект, созданный контейнером. Аннотация лишь помогает контейнеру понять, что объект нужно создать. Условно говоря, SlugNormalizer.class — это просто байт-код и метаданные, а SlugNormalizer как бин — это конкретный экземпляр, который лежит в ApplicationContext и может быть «выдан» или «внедрён» как зависимость.

3. @Service, @Repository, @Controller: роли компонентов

Когда проект маленький, можно было бы жить и на одном @Component. Но очень быстро читать код становится тяжело: что из этого сервис? что репозиторий? что слой web? что просто хелпер? Поэтому Spring предлагает более «говорящие» стереотипы: @Service, @Repository, @Controller.

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

Сравним эти аннотации как «бейджики профессий»:

Аннотация Для чего обычно используется Что даёт (помимо регистрации)
@Component Общий компонент без явной роли Просто «это бин»
@Service Сервисный слой, прикладная логика Читаемость: «тут сервис»
@Repository Доступ к данным (даже если данные in-memory) Читаемость и (в полном Spring) полезные инфраструктурные эффекты
@Controller Web-слой (MVC контроллеры) Читаемость и участие в web-инфраструктуре

Сейчас нам важен не web и не база, а именно логика ролей. В нашем catalog-service эти роли выглядят естественно: репозиторий хранит и отдаёт данные каталога, сервис решает «как» это делать, а контроллер нужен здесь не ради HTTP-деталей, а чтобы сразу был виден отдельный web-слой. Возьмём тот же мини-граф: CourseCatalogRepository — это контракт доступа к данным каталога, а InMemoryCourseCatalogRepository — его конкретная in-memory реализация.

Мини-скелеты таких классов могут выглядеть так.

Сервис:

package com.example.catalogservice.catalog.service;

import com.example.catalogservice.catalog.repository.CourseCatalogRepository;
import org.springframework.stereotype.Service;

@Service // Роль: прикладная логика (сервисный слой)
public class CourseCatalogService {

    public CourseCatalogService(CourseCatalogRepository repository) {
        // DI через конструктор: Spring сам передаст бин репозитория,
        // который реализует нужный контракт
        // Здесь намеренно нет логики: это «скелет», чтобы увидеть роль и принцип внедрения
    }
}

Конкретная in-memory реализация репозитория (пока просто заглушка):

package com.example.catalogservice.catalog.repository;

import java.util.List;
import org.springframework.stereotype.Repository;

@Repository // Роль: доступ к данным (пока конкретная in-memory реализация контракта)
public class InMemoryCourseCatalogRepository implements CourseCatalogRepository {

    @Override
    public List<String> findAllSlugs() {
        // Заглушка: позже здесь появятся реальные данные (например, коллекция, БД и т.д.)
        return List.of("spring-boot", "spring-core");
    }
}

Контроллер (пока пустой, просто чтобы увидеть роль):

package com.example.catalogservice.catalog.web;

import org.springframework.stereotype.Controller;

@Controller // Роль: web-слой (входная точка для HTTP/MVC), пока без методов
public class CourseCatalogController {
    // Пусто намеренно: сейчас нам важен только маркер роли
}

Здесь особенно важно не попасть в ловушку: «ага, раз @Service, значит там должна быть бизнес-логика». Нет. Аннотация не заставляет вас писать хорошую логику. Она лишь делает роль класса явной и даёт Spring повод зарегистрировать бин.

Ещё одна важная деталь: эти аннотации существуют не ради красоты. В реальных проектах соглашения работают как язык команды. Когда вы видите @Repository, вы ожидаете слой доступа к данным. Когда видите @Controller, ожидаете входную точку web-слоя. Когда видите @Service, ожидаете «центр прикладной логики», а не «класс с пятью статическими методами на все случаи жизни». И такие ожидания — это тоже часть архитектуры.

4. Component scanning: как Spring ищет компоненты по пакетам

Мы подошли к механизму, который делает все эти аннотации полезными: component scanning. Он отвечает на практический вопрос: «как контейнер вообще узнаёт, что у меня есть CourseCatalogService или SlugNormalizer, и что их нужно зарегистрировать?»

Component scanning — это процесс, при котором Spring на старте приложения просматривает классы в определённых пакетах и ищет среди них кандидатов на бины. Кандидатами становятся классы, помеченные нужными аннотациями, например @Component, @Service, @Repository, @Controller. Дальше Spring регистрирует эти классы в контейнере, а затем создаёт экземпляры и связывает зависимости.

Если упростить до «картины на салфетке», процесс выглядит примерно так:

flowchart TD
    A["Запуск приложения"] --> B["Определяем base package (откуда сканировать)"]
    B --> C["Сканируем классы в пакетах и подпакетах"]
    C --> D["Находим классы со stereotype-аннотациями"]
    D --> E["Регистрируем bean definitions в контейнере"]
    E --> F["Создаём бины и выполняем wiring зависимостей"]
    F --> G["ApplicationContext готов"]

Обратите внимание на ключевой момент: scanning происходит не «по всей файловой системе», а по пакетам. Для Java это естественно, потому что структура пакетов обычно соответствует структуре каталогов в src/main/java. Spring ориентируется на package name, а не на то, «в какой папке вам нравится держать классы».

И вот тут возникает очень практическая истина, из которой выросло огромное количество багов на ранних стадиях обучения: если ваш класс лежит вне зоны сканирования, для Spring его не существует. Он может прекрасно компилироваться. IDE может его видеть. Вы даже можете вызвать его вручную через new. Но контейнер его не зарегистрирует, и DI на нём не сработает.

Чтобы почувствовать разницу, полезно держать в голове три состояния класса:

  • класс существует в коде;
  • класс помечен как компонент;
  • класс найден сканированием и зарегистрирован как бин.

Пока не выполнены пункты 2 и 3 — или не использован другой способ регистрации, — контейнер просто не будет знать, что это «часть приложения».

5. Базовый пакет: влияние расположения main-класса

Слово «base package» звучит как что-то скучное, но по факту это один из главных рычагов того, «видит» ли Spring ваш код. В типичном Spring Boot-приложении базовый пакет сканирования определяется очень простым правилом: Spring стартует сканирование от пакета, в котором лежит main-класс приложения — тот самый класс с @SpringBootApplication.

В нашем курсе мы хотим, чтобы catalog-service имел читаемую структуру пакетов. Примерно такую — пока без деталей, просто как ориентир:

com.example.catalogservice
├─ CatalogServiceApplication
├─ catalog
│  ├─ repository
│  ├─ service
│  └─ web
└─ config

Если CatalogServiceApplication лежит в пакете com.example.catalogservice, то scanning «увидит» всё, что лежит ниже по дереву пакетов: com.example.catalogservice.catalog.*, com.example.catalogservice.config.* и так далее.

Вот нормальный «корневой» main-класс:

package com.example.catalogservice;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // Эта аннотация включает автоконфигурацию и component scanning от пакета com.example.catalogservice
public class CatalogServiceApplication {
    // Важно: положение этого класса задаёт «зонтик» base package для сканирования
}

А вот типичная ловушка новичка: вы положили main-класс слишком глубоко, например в com.example.catalogservice.app, потому что «ну это же приложение, пусть будет app». В результате базовый пакет сканирования станет com.example.catalogservice.app, а пакет com.example.catalogservice.catalog окажется соседним, а не вложенным. То есть component scanning туда уже не дойдёт.

package com.example.catalogservice.app;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication // Base package станет com.example.catalogservice.app, и соседние пакеты сканироваться не будут
public class CatalogServiceApplication {
    // Эта версия выглядит «почти так же», но результат для component scanning будет совсем другим
}

Чем это заканчивается на практике? Очень предсказуемо. Вы пишете @Service:

package com.example.catalogservice.catalog.service;

import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {
}

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

... required a bean of type 'CourseCatalogService' that could not be found

И это не «Spring сломался». Это Spring честно говорит: «я не видел этот класс при сканировании, поэтому не зарегистрировал бин».

Самый здоровый вывод из этого раздела: main-класс должен лежать достаточно высоко в иерархии пакетов, чтобы быть «зонтиком» над всем приложением. Для нашего catalog-service это пакет com.example.catalogservice, как в целевой структуре проекта. Чем раньше вы привыкнете к этому правилу, тем меньше у вас будет ощущения «Spring — магия и лотерея».

Технически Spring позволяет настраивать зоны сканирования вручную, но для начинающего это чаще не решение, а способ усложнить себе жизнь. В учебном проекте гораздо проще и правильнее держать main-класс в корневом пакете.

6. Диагностика и самопроверка

На этом этапе обучения хочется, чтобы ошибка выглядела так: «Дорогой разработчик, ты забыл аннотацию, исправь строку 12». К сожалению — или к счастью, если вы любите приключения, — Spring чаще сообщает иначе: «не могу создать бин X, потому что ему нужен бин Y, а Y не найден». Для новичка это звучит как загадка сфинкса, но на самом деле диагностика довольно приземлённая.

Чаще всего причины две. Либо класс не помечен stereotype-аннотацией, либо он лежит вне зоны component scanning. Иногда это ещё и банальная опечатка в пакете или ситуация, когда вы создали класс, но он не попал в сборку — например, не тот source root, — но это обычно видно по IDE.

Для отладки полезно помнить, что ApplicationContext — это «реестр бинов». Если бин зарегистрирован, контекст может его отдать. Но здесь важно не переучиться в плохую сторону: getBean(...) — это инструмент диагностики и демонстрации, а не стиль написания прикладного кода.

Вот маленький кусочек кода, который показывает сам принцип. В реальном проекте вы не будете так писать бизнес-код — это именно «проверка, что бин существует»:

import org.springframework.context.ApplicationContext;

// ctx — ваш ApplicationContext (например, полученный в тесте или из точки старта приложения)
SlugNormalizer normalizer = ctx.getBean(SlugNormalizer.class); // Если бина нет — проблема в регистрации/сканировании
System.out.println(normalizer.normalize("  Spring-Boot ")); // spring-boot

Если эта строка падает с ошибкой «нет такого бина», значит либо SlugNormalizer не был зарегистрирован, либо вы запускаете приложение не тем контекстом, либо класс находится вне сканирования. И тогда вы возвращаетесь к простому чек-листу в голове — без распечатки на бумаге, мы всё-таки в IT: аннотация на месте? пакет под base package? main-класс в правильном пакете?

Ещё одно очень приземлённое правило: если Spring пишет, что «не нашёл бин», значит, он действительно его не нашёл. Это не намёк, не философия и не тест на характер. Это просто факт.

7. Типичные ошибки: stereotypes и scanning

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

Ошибка №1: «Я создал класс, значит Spring обязан его найти».
Это очень человеческое ожидание: раз класс есть в проекте, значит контейнер его увидит. Но Spring не регистрирует бины из воздуха. Если на классе нет stereotype-аннотации — или другого способа регистрации, — то для контейнера это просто класс на classpath, не более. В итоге DI не работает, и вы получаете «bean not found». Лечится это почти всегда одной мыслью: «контейнер управляет только теми объектами, которые явно зарегистрированы».

Ошибка №2: «Я поставил @Service, а оно всё равно не работает — значит аннотация сломалась».
Аннотация тут обычно ни при чём. Чаще всего проблема в зоне сканирования: класс помечен, но лежит вне base package. В результате Spring его не регистрирует, и дальше всё падает цепочкой. Этот баг особенно коварен, если вы случайно положили main-класс слишком глубоко и часть пакетов стала «соседней», а не вложенной. Тогда половина приложения видна Spring’у, половина — нет, и ощущение действительно такое, будто «магия то работает, то нет».

Ошибка №3: «Помечу всё @Component, чтобы точно работало».
Так действительно часто «будет работать», но читаемость проекта начнёт страдать очень быстро. Когда у всех классов один и тот же бейджик, вы теряете семантику. @Service, @Repository, @Controller существуют не потому, что Spring любит плодить аннотации, а потому, что люди любят понимать код. Хороший стиль: @Component — для общего, @Service — для сервиса, @Repository — для слоя данных, @Controller — для web-слоя.

Ошибка №4: «Положу класс в пакет external.*, чтобы было красиво».
Само слово external в пакете ничего плохого не делает, но часто это сигнал: «класс вне нашей основной структуры». И очень легко получить ситуацию, когда пакет оказывается вне component scanning. Тогда класс не найден, бин не создан, DI не работает. В учебном проекте почти всегда лучше держать компоненты внутри корневого пакета приложения, под зонтиком main-класса, чтобы scanning был предсказуемым.

Ошибка №5: «Раз Spring-контейнер умный, я буду везде таскать ApplicationContext и доставать бины руками».
Технически это возможно, и иногда новичок так «чинит» проблему: вместо того чтобы настроить scanning, он начинает делать context.getBean(...) в прикладных классах. Это превращает контейнер в глобальную точку доступа — service locator — и быстро ломает сам смысл DI. Правильный подход почти всегда обратный: сделать зависимости явными, через конструктор, а не прятать их за «магической выдачей» из контекста.

1
Задача
Spring Boot, 2 уровень, 2 лекция
Недоступна
Компонент в базовом пакете
Компонент в базовом пакете
1
Задача
Spring Boot, 2 уровень, 2 лекция
Недоступна
Роли `@Repository`, `@Service` и `@Controller`
Роли `@Repository`, `@Service` и `@Controller`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ