1. Когда @Component мало
Представьте: вы приходите в гараж, а на всех коробках наклейка «ШТУКА». Вроде честно: внутри действительно штука. Но попробуйте быстро найти «ключ на 13» — почувствуете примерно то же, что новый разработчик в проекте, где всё помечено только @Component. Технически контейнеру всё равно, а человеку — нет: мы читаем проект глазами и пытаемся понять роли классов, а не просто факт «этот класс зарегистрирован».
Когда мы включаем scanning, регистрация переезжает «рядом с кодом». Это хорошо. Но если ставить @Component вообще на всё подряд, у нас остаётся ровно один смысловой сигнал: «контейнер это видит». А дальше начинается квест: это сервис? хранилище? конфигурация? инфраструктурная мелочь? В маленьком проекте такое ещё можно вывезти. В среднем — уже тяжело.
И вот тут стереотипные аннотации работают как семантические ярлыки. Они помогают читать проект, помогают держать дисциплину слоёв и дают контейнеру вместе с инструментами вокруг дополнительные подсказки: «вот сервис», «вот репозиторий», «вот конфиг». Важно: сейчас нас интересует именно смысл роли, а не дополнительные механики вокруг этих аннотаций.
2. Стереотипные аннотации и @Component
Если сказать просто, стереотипная аннотация — это тот же @Component, но с уточнением роли. Внутри Spring это устроено через мета-аннотации: аннотация @Service сама помечена @Component, @Repository — тоже, и так далее. Поэтому scanner «видит» их так же, как и обычный компонент.
Полезно один раз увидеть это глазами — без чтения исходников Spring на ночь. В Java аннотации — это тоже классы, и у них тоже могут быть аннотации. Можно буквально спросить у JVM:
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
public class StereotypeMetaDemo {
public static void main(String[] args) {
// Проверяем не наш класс, а саму аннотацию @Service:
// есть ли на ней meta-annotation @Component?
System.out.println(Service.class.isAnnotationPresent(Component.class)); // true
}
}
Мы проверили не наш класс, а саму аннотацию @Service. И получили true: @Service действительно «наследует смысл» компонента через мета-аннотацию.
При этом ваш сервисный класс не обязан быть помечен @Component напрямую. Достаточно @Service, а Spring уже поймёт это как «компонент сервисной роли». Именно поэтому scanning работает одинаково.
Чтобы закрепить идею, удобно держать в голове такую мини-карту:
flowchart TD
%% Stereotype-аннотации — это "ярлыки роли", которые всё равно сводятся к @Component
A["@Service / @Repository / @Controller / @Configuration"] --> B["@Component (meta-annotation)"]
B --> C["component scanning"]
C --> D["BeanDefinition"]
D --> E["bean instance в ApplicationContext"]
И ещё одна важная мысль: стереотип — это не «архитектура сама по себе». Это подсказка, которая помогает архитектуру удерживать. Если класс по смыслу не сервис, не надо делать вид, что он сервис, только потому что вам нравится слово @Service. Это как подписать кружку «чай», а наливать туда компот. Сначала смешно, потом все страдают.
3. @Service: сервисный слой
Сервис в нашем учебном проекте — не «слой, где можно делать всё». Скорее это место, где живёт сценарий (use case): создать заказ, отменить заказ, посчитать цену, запустить отправку уведомления. Хороший @Service обычно оркестрирует действия и вызывает зависимости, но не превращается в монстра на 2000 строк. И да, сервис — это всё ещё обычный POJO, просто контейнер знает, что перед ним сервис.
В ContextFlow логично помечать @Service классы из application слоя: OrderPlacementService, OrderCancellationService, OrderPricingService и т.п. Вы прямо в коде показываете: «это часть бизнес-потока приложения». Для новичка это очень полезно: открываешь класс и уже понимаешь, что перед тобой центральная логика, а не техническая мелочь.
Мини-пример сервиса. Здесь нам важнее сама роль класса, поэтому механику выбора кандидатов пока оставим за кадром:
package com.example.contextflow.application.service;
import com.example.contextflow.domain.ports.OrderStore;
import org.springframework.stereotype.Service;
@Service
public class OrderPlacementService {
// Зависимость на порт доменного слоя: сервис "оркестрирует",
// а хранение/транспорт деталей прячется за интерфейсом.
private final OrderStore orderStore;
public OrderPlacementService(OrderStore orderStore) {
// DI через конструктор — самый прямой и проверяемый вариант.
this.orderStore = orderStore;
}
public void place(String orderId) {
// Сценарий: сохранить заказ через порт.
orderStore.save(orderId);
// Для примера выводим результат "на экран" (в реальном проекте тут был бы логгер).
System.out.println("Created order " + orderId); // Created order ORD-1
}
}
Что здесь важно именно в контексте @Service, а не стиля инъекции: когда вы видите @Service, вы ожидаете, что это сервис приложения, а не хранилище, не конфигурация и не доменный объект. То есть это не столько «инструкция для Spring», сколько честный дорожный знак для людей.
Небольшой, но полезный нюанс: @Service можно использовать и как «якорь» для технических механизмов — например, позже аспекты часто нацеливают именно на сервисы. Но это бонус, а не главная причина. Главная причина — читаемость и дисциплина.
4. @Repository: слой хранения
Слово «repository» у многих сразу вызывает образы базы данных, SQL, Hibernate и трёхчасового разговора про индексы. Спокойно: в нашем курсе базы данных пока нет. Но слой хранения уже есть. И InMemoryOrderStore — это как раз он: место, где данные лежат, откуда их можно достать, посчитать и обновить. В боевом проекте это могла бы быть БД, а в учебном — Map и здравый смысл.
Смысл @Repository в том, чтобы маркировать класс как компонент доступа к данным. Даже если данные лежат в памяти, для бизнес-логики это всё равно внешняя зависимость. Бизнес-сервис должен уметь работать с портом OrderStore, а конкретная реализация (InMemory...) — прятаться в инфраструктуре.
Чтобы не тащить сюда весь сценарий создания заказа, возьмём короткий контракт хранения. Здесь нам важна сама роль слоя: бизнес-сервис работает с портом, а конкретная реализация остаётся в инфраструктуре.
Сначала покажем порт (интерфейс). Он — не Spring-объект, он — контракт:
package com.example.contextflow.domain.ports;
// Порт: минимальный контракт хранения для разговора о роли repository.
// Здесь намеренно нет Spring-аннотаций — это всё ещё обычный контракт.
public interface OrderStore {
void save(String orderId);
int count();
}
А теперь реализация-хранилище, которую контейнер обнаружит через scanning:
package com.example.contextflow.infrastructure.store;
import com.example.contextflow.domain.ports.OrderStore;
import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Repository
public class InMemoryOrderStore implements OrderStore {
// Простейшее хранилище в памяти.
// ConcurrentHashMap — чтобы даже в многопоточности пример не рассыпался.
private final Map<String, String> orders = new ConcurrentHashMap<>();
// Сохранение заказа: "кладём" id как ключ и как значение (для учебного примера достаточно).
public void save(String orderId) { orders.put(orderId, orderId); }
// Подсчёт количества "сохранённых" заказов.
public int count() { return orders.size(); }
}
С точки зрения Spring сейчас это почти то же самое, что и @Component. Но для чтения проекта смысловая разница огромна: @Repository говорит «я храню данные». Даже если это просто данные в Map, это всё равно слой хранения.
Есть и дополнительный технический смысл — упомянем аккуратно, без ухода в data-курс. Исторически Spring умеет делать так называемую exception translation для @Repository, то есть превращать низкоуровневые исключения доступа к данным в более единый набор исключений Spring. В нашем примере с хранением в памяти это почти не проявится, но полезно знать, что аннотация — не просто декорация.
5. Другие стереотипные аннотации
@Controller: веб-граница
@Controller часто пугает новичков: кажется, будто стоит её поставить — и у вас внезапно «поднимется сервер», «появятся эндпоинты», «придёт REST». Нет. Аннотация не открывает портал в параллельную вселенную. Она всего лишь маркирует класс как контроллер в терминах Spring MVC. Чтобы это стало чем-то большим, нужны веб-зависимости и соответствующее окружение, которых в нашем курсе сознательно нет.
Почему тогда мы вообще её упоминаем? Потому что в реальных проектах вы будете видеть @Controller постоянно. И важно сразу правильно понимать её роль: это UI/web-граница приложения в мире HTTP, а не «ещё один сервис».
В чистом Spring Core приложении без Spring MVC @Controller технически просто станет bean-ом при scanning, потому что это тоже stereotype. Но никакой «магии обработки HTTP» у него не появится: рядом нет веб-инфраструктуры.
Чтобы понять, как выглядит контроллер по форме, достаточно миниатюрного примера. Обратите внимание: код компилируется, потому что сама аннотация есть, но на практике в нашем проекте мы так делать не будем.
package com.example.contextflow.web;
import org.springframework.stereotype.Controller;
@Controller
public class OrderController {
// В web-проекте здесь были бы handler-методы и маппинги,
// но в этом курсе web-стека нет, поэтому это просто bean.
}
Запомните одну здоровую мысль: аннотации не заменяют зависимости. Если у вас нет spring-webmvc, MVC не появится. Если у вас нет базы данных, не появится JPA. Spring не умеет «додумывать реальность» по аннотации (а жаль: поставил @Money — и зарплата пришла).
@Configuration: роль конфигурации
С @Configuration ситуация особенная: это тоже stereotype, тоже участвует в компонентной модели Spring, но смысл у него отличается сильнее, чем у @Service и @Repository. @Configuration обычно говорит: «этот класс описывает, как собирать приложение». То есть это не бизнес-логика и не хранение данных, а контейнерный композиционный слой — wiring на стороне Spring.
На этом этапе нам важны два практических факта. Во‑первых, @Configuration — хороший способ обозначить класс, который отвечает за настройки контекста, например за @ComponentScan. Во‑вторых, Spring относится к нему не совсем как к обычному @Component, потому что этот класс участвует в сборке контейнера. Сейчас нам важнее не внутренняя механика, а сам смысл этого ярлыка.
Самый базовый AppConfig, который включает scanning, выглядит так:
package com.example.contextflow.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
// Указываем базовый пакет, откуда Spring будет сканировать компоненты.
@ComponentScan("com.example.contextflow")
public class AppConfig {
}
Обратите внимание на архитектурный сигнал: конфигурация живёт отдельно и занимается инфраструктурой сборки. Если вы начнёте писать в @Configuration бизнес-методы вроде «создать заказ», это будет похоже на хранение макарон в системном блоке. Технически можно, но потом вы сами не захотите это объяснять.
6. Карта ролей ContextFlow
Когда вы начинаете использовать стереотипы, у команды появляется очень полезный эффект: по аннотации и пакету вы заранее ожидаете определённое поведение класса. Это не компилятор, но это дисциплина. Примерно как договорённость: «в этом ящике инструменты, а не печенье». Никто не запрещает положить туда печенье, но… зачем.
Ниже — компактная карта, которую можно держать в голове для ContextFlow и для большинства обычных приложений:
| Роль в системе | Типичный стереотип | Пример из ContextFlow | Что вы ожидаете, открывая класс |
|---|---|---|---|
| Сценарий/оркестрация | @Service | OrderPlacementService | «Здесь происходит бизнес-поток: вызовы зависимостей, правила сценария» |
| Хранение данных | @Repository | InMemoryOrderStore | «Здесь живут операции сохранения и чтения данных» |
| Техническая утилита/инфраструктура | @Component | ClockProvider, ScenarioRunner | «Это вспомогательная штука: не сервис и не хранилище» |
| Веб-граница (HTTP) | @Controller | (в нашем курсе отсутствует) | «Это обработчик запросов; без веб-стека это просто bean» |
| Сборка контекста | @Configuration | AppConfig | «Здесь собирается и настраивается контейнер» |
Чтобы увидеть, что всё это действительно работает через scanning, можно запустить приложение через контекст и убедиться, что @Service-класс находится без ручной регистрации:
package com.example.contextflow;
import com.example.contextflow.application.service.OrderPlacementService;
import com.example.contextflow.config.AppConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApp {
public static void main(String[] args) {
// Поднимаем контекст из Java-конфига.
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Достаём сервис из контейнера и запускаем сценарий.
context.getBean(OrderPlacementService.class).place("ORD-1");
}
}
}
Если хочется совсем «проверки глазами», можно в main() временно вывести, какие бины помечены именно @Service:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Service;
public class BeanRoleDebug {
public static void main(String[] args) {
// Диагностический код: удобно для обучения, но не для боевого приложения.
try (var context = new AnnotationConfigApplicationContext(com.example.contextflow.config.AppConfig.class)) {
// Spring умеет искать бины по наличию аннотации на классе.
System.out.println(context.getBeansWithAnnotation(Service.class).keySet());
// например: [orderPlacementService]
}
}
}
Это не повседневный стиль — не надо превращать приложение в «само-ищу-бинов», — но для диагностики и обучения это отлично: вы видите, что стереотип живёт не только в ваших глазах, но и в метаданных контекста.
7. Типичные ошибки при работе со stereotype-аннотациями
После первой встречи со стереотипными аннотациями появляется опасный соблазн: «О, теперь я буду архитектором: раздам всем красивые ярлыки». Порыв понятный, но у него есть классические грабли. Ниже — ошибки, которые новички чаще всего делают именно на этом этапе, когда scanning уже включён, а культура проекта ещё не устоялась.
Ошибка №1: помечать всё подряд @Service, потому что «это же важный класс».
@Service не означает «важно». Он означает «сервисная роль». Если вы пометили ClockProvider, IdGenerator, JsonFormatter и InMemoryOrderStore как @Service, то через месяц слово «Service» перестанет нести смысл. Проект снова станет набором «ШТУК», просто в другой упаковке.
Ошибка №2: ставить stereotype-аннотации на доменные модели (Order, Customer, OrderItem).
Доменные объекты обычно живут коротко: создали — обработали — забыли. Делать их bean-ами означает затаскивать в контейнер то, что должно оставаться обычными объектами. В итоге вы получаете непредсказуемую жизнь объектов, лишние зависимости и странные попытки «получить заказ из контекста». Заказ должен приезжать как данные, а не как singleton-bean.
Ошибка №3: ожидать, что @Repository «автоматически делает базу данных».
@Repository — это ярлык роли, а не подключение PostgreSQL по Wi‑Fi. Если у вас в проекте Map, то это repository, который хранит данные в памяти. Если позже появится БД, вы поменяете реализацию, но бизнес-сервисы продолжат зависеть от порта. Стереотип помогает держать границу, но не создаёт инфраструктуру из воздуха.
Ошибка №4: думать, что @Controller превращает приложение в веб-сервис.
Аннотация без веб-стека — это просто аннотация. Чтобы появились HTTP-запросы, роутинг и обработка @GetMapping, нужен Spring MVC (и обычно Spring Boot, но это уже следующий курс). В нашем ContextFlow контроллеры не нужны, и даже если вы случайно поставите @Controller, он просто станет очередным bean-ом, не более.
Ошибка №5: складывать конфигурацию и бизнес-логику в один класс, потому что «так быстрее».
@Configuration должен оставаться местом, где вы настраиваете контейнер. Если вы начинаете писать там бизнес-методы, уровни смешиваются: конфигурация начинает «делать работу приложения», а приложение — зависеть от деталей конфигурации. На маленьком примере это не так заметно, но именно так рождаются «магические» проекты, где никто толком не понимает, что с чем связано.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ