JavaRush /Курсы /Spring Core /Какие объекты делать bean-ами

Какие объекты делать bean-ами

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

1. Bean — управляемый контейнером объект

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

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

Сразу зафиксируем мысль, от которой новичкам обычно легче дышать: bean не обязан быть особенным классом. Это может быть самый обычный POJO — без наследования от Spring-классов, без магии, без интерфейсов SomethingAware и без «extends FrameworkMonster». Bean — это не «тип класса», а статус объекта: контейнер знает про него и управляет им.

Если позволить себе совсем бытовую аналогию, то контейнер — это «отдел кадров», а beans — «штатные сотрудники». Контейнер знает, кто кому подчиняется (зависимости), кого нужно нанять первым (порядок создания) и кто вообще нужен, чтобы приложение могло работать. А вот доменные объекты вроде Order — не сотрудники, а «документы», которые сотрудники создают и обрабатывают в процессе работы. Отдел кадров не заводит на каждый документ отдельное личное дело.

Полезное наблюдение, которое пригодится уже сегодня: контейнер управляет bean-ами через BeanDefinition. То есть до того, как объект стал bean-ом, в контейнере появляется его описание: «вот такой класс (или фабричный метод), вот такие зависимости». А если объект создаётся через new прямо в вашем коде, контейнер о нём просто не узнает — и это нормально.

Небольшой пример на уровне «почувствовать руками»:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

// Контекст — «фабрика» и «каталог» для bean-ов
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Этот объект создаётся контейнером и живёт в его управлении
    OrderPlacementService service = context.getBean(OrderPlacementService.class); // bean

    // Этот объект мы создаём сами: это данные конкретного сценария
    Order order = new Order("o-1", OrderStatus.NEW);                              // обычный объект

    System.out.println(service.getClass().getSimpleName()); // OrderPlacementService
    System.out.println(order.status());                     // NEW
}

Здесь service приходит из контейнера — это часть «скелета приложения». А order мы создаём сами: это конкретные данные конкретного сценария. И никакой внутренней боли у Spring от этого нет — он не ревнует.

2. Граница контейнера и выбор beans

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

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

Почему это важно не только философски, но и на практике:

Если отдать контейнеру всё подряд, конфигурация быстро превращается в каталог случайных сущностей. Становится сложнее понять, что здесь «каркас приложения», а что просто данные. А ещё вы довольно быстро упрётесь в объекты, которые невозможно честно сделать bean-ами без странных костылей, потому что им нужны данные, известные только во время выполнения сценария (например, customerId, список товаров, текущая дата в отчёте и так далее).

Если же, наоборот, bean-ов будет слишком мало и вы продолжите создавать сервисы вручную, вы откатитесь назад к боли первых дней: зависимости снова начнут прятаться, composition root расползётся, а подмена реализаций станет неприятной.

В нашем проекте ContextFlow мы хотим ту самую золотую середину: контейнер управляет сервисами и инфраструктурой, а доменные объекты живут как нормальные Java-объекты. Тогда проект остаётся понятным: Spring помогает собирать и обслуживать объектный граф, но не пытается быть «операционной системой для каждого new».

Чтобы закрепить идею, вот небольшая таблица с «человеческим» смыслом:

Тип объекта Пример в ContextFlow Живёт сколько? Должен быть bean-ом? Почему
Сервис / use-case OrderPlacementService Долго (пока живёт приложение) Да Это часть каркаса, имеет зависимости, удобно собрать один раз
Инфраструктура InMemoryOrderStore Долго Да Это «внешний мир» для бизнеса, удобно подменять, удобно конфигурировать
Точка входа сценария ScenarioRunner Долго Да Это оркестратор сценария, и ему нужны готовые зависимости
Доменная сущность Order По мере сценариев Нет Их много, они данные, а не инфраструктура
Команда CreateOrderCommand Мгновенно / до конца вызова Нет Это «пакет входных данных», который создаётся в момент действия
Промежуточный результат BigDecimal totalAmount Мгновенно Нет Это вычисление, а не компонент приложения

Обратите внимание: слово «долго» здесь не про бессмертие объекта, а про то, что объект не привязан к одному конкретному заказу. Он нужен как механизм, а не как данные.

3. Beans в ContextFlow: сервисы и инфраструктура

Когда мы говорим «bean — это управляемый объект», сразу хочется спросить: «Окей, а кого конкретно берём в штат?». В этом разделе применим понятие границы к нашему ContextFlow и перечислим типичных кандидатов в beans: сервисы, точки входа, порты (интерфейсы) и их реализации. Нас сейчас интересует одна логика: что именно контейнеру имеет смысл собирать самому.

С точки зрения архитектуры ContextFlow на данном этапе похож на небольшое консольное приложение с сервисным слоем: есть сценарий (создать заказ), есть сервис, который его оркестрирует, и есть инфраструктурные зависимости (хранилище, аудит, уведомления, генератор идентификаторов). Все эти штуки — «механизмы», которые удобно собрать один раз и дальше переиспользовать.

Типичный список beans на текущей стадии проекта выглядит так:

ScenarioRunner — потому что это точка входа нашего приложения. Он не должен вручную собирать сервисы: его задача — просто запуститься уже готовым.

OrderPlacementService (и другие сервисы) — потому что это бизнес-оркестратор, который зависит от портов и инфраструктуры.

OrderStore, AuditWriter, NotificationSender, OrderIdGenerator — потому что это точки вариативности и внешние зависимости. Сегодня это простые реализации в памяти и через консоль, позже в рамках курса они всё равно будут меняться (в разных профилях, конфигурациях и т.п.), и Spring-контейнер тут как раз для этого.

Эту укрупнённую схему полезно держать в голове и дальше: ContextFlowApplication -> ScenarioRunner -> OrderPlacementService -> порты и инфраструктура, а команды и доменные объекты остаются обычным Java-кодом.

Покажем это на небольшом живом фрагменте. Метод main для Spring Core приложения почти идеален в своей простоте: создать контекст, взять один стартовый bean и запустить его.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApplication {

    public static void main(String[] args) {
        // На старте приложения создаём контекст: он соберёт bean-ы по конфигурации
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Берём «точку входа» сценария как готовый bean
            ScenarioRunner runner = context.getBean(ScenarioRunner.class);
            runner.run(); // Запускаем бизнес-сценарий
        }
    }
}

Заметьте, main здесь не занимается бизнесом. Он не строит Order, не считает скидки, не выбирает каналы уведомлений. Его задача — сказать: «Контейнер, собери приложение. Мне нужна стартовая точка».

Теперь — фрагмент конфигурации (мы не пытаемся сделать её идеальной; сейчас важно понять сам принцип):

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

@Configuration // Говорим Spring: это класс конфигурации, из него нужно собрать bean-ы
public class AppConfig {

    @Bean // Bean инфраструктуры: «механизм» хранения, который удобно подменять
    public OrderStore orderStore() {
        return new InMemoryOrderStore();
    }

    @Bean // Bean точки входа: контейнер сам передаст сюда нужный сервис
    public ScenarioRunner scenarioRunner(OrderPlacementService service) {
        return new ScenarioRunner(service);
    }
}

Да, тут пока не хватает @Bean для OrderPlacementService и других зависимостей — в реальном проекте они, конечно, будут. Но даже этот фрагмент показывает главное: bean-ами становятся «узлы каркаса», а не «данные сценария».

Отдельно полезно увидеть, что сам ScenarioRunner — обычный класс. Ему не обязательно знать, что его создаёт Spring. Его зависимость видна в конструкторе — это тот самый DI-подход, который мы уже освоили.

public class ScenarioRunner {

    private final OrderPlacementService orderPlacementService;

    public ScenarioRunner(OrderPlacementService orderPlacementService) {
        // Зависимость передаётся снаружи (контейнером) — это и есть DI
        this.orderPlacementService = orderPlacementService;
    }

    public void run() {
        System.out.println("Scenario started"); // Scenario started
        // Дальше будет создание обычных объектов сценария (команд, доменных сущностей)
    }
}

И это уже важная победа: стартовый объект создаётся контейнером, а дальше он запускает сценарий обычным Java-кодом.

4. Что не делать bean-ами: доменные сущности и команды

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

В ContextFlow доменная модель специально «тонкая»: Order, OrderItem, Customer, статусы заказа, команды на создание/отмену. Эти объекты существуют, потому что бизнес-сценарий оперирует ими как данными. Они не должны иметь зависимостей на OrderStore или NotificationSender — иначе доменная модель начнёт протекать инфраструктурой, а это почти всегда боль.

Вот пример команды. Это типичный «мешочек данных» на входе в сценарий:

import java.util.List;

// Команда — это входные данные сценария (обычно создаётся в момент вызова use-case)
public record CreateOrderCommand(
        String customerId,
        List<OrderItem> items
) {
}

Команда не просит контейнер о помощи. Она просто хранит информацию, которую пользователь или сценарий передал в use-case. Поэтому её естественно создавать там, где у нас уже есть конкретные данные: в ScenarioRunner или позже внутри теста.

То же самое и с доменной сущностью. Она описывает заказ как факт:

import java.time.Instant;

// Доменная сущность — это «данные бизнеса», а не инфраструктурный компонент
public record Order(
        String id,
        String customerId,
        OrderStatus status,
        Instant createdAt
) {
}

В реальном проекте у заказа будет больше полей, но суть не меняется: это данные. Их не «настраивает контейнер» — они появляются в момент обработки сценария.

И вот важный момент: таких заказов будет много. Если у нас есть 100 заказов, будет 100 объектов Order. Контейнер не должен знать про каждый из них и не должен пытаться их «держать». Контейнер держит механизмы, которые умеют с заказами работать.

Как это выглядит в сценарии:

import java.util.List;

public class ScenarioRunner {

    private final OrderPlacementService orderPlacementService;

    public ScenarioRunner(OrderPlacementService orderPlacementService) {
        this.orderPlacementService = orderPlacementService;
    }

    public void run() {
        // Команда создаётся на месте, потому что данные известны только в момент сценария
        CreateOrderCommand cmd = new CreateOrderCommand(
                "cust-1",
                List.of(new OrderItem("PEN", 2))
        );

        // Сервис (bean) обрабатывает команду и возвращает доменный результат (обычный объект)
        Order created = orderPlacementService.placeOrder(cmd);
        System.out.println(created.status()); // NEW
    }
}

CreateOrderCommand и OrderItem создаются через new или конструктор record прямо в сценарии. Это нормально, это правильно и это не «возврат к ручной сборке». Ручная сборка — это когда вы вручную собираете каркас приложения (сервисы и инфраструктуру). А создавать доменные объекты по ходу работы — обычная жизнь приложения.

5. Как beans и обычные объекты связаны в сценарии

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

Сначала — общая схема. Её полезно держать в голове, когда вы не понимаете, «почему этот объект не в контейнере».

flowchart LR
  subgraph C[Spring ApplicationContext]
    SR["ScenarioRunner (bean)"]
    OPS["OrderPlacementService (bean)"]
    GEN["OrderIdGenerator (bean)"]
    STORE["OrderStore (bean)"]
  end

  SR -->|создаёт| CMD["CreateOrderCommand (plain)"]
  CMD --> OPS
  OPS --> GEN
  OPS --> STORE
  OPS -->|создаёт| O["Order (plain)"]
  STORE -->|хранит| O

Тут видно главное: контейнер управляет «механизмами» (левый блок), а CreateOrderCommand и Order живут «снаружи» как данные.

Теперь кусок сервиса. Он — bean, потому что имеет зависимости и является частью каркаса:

import java.time.Instant;

public class OrderPlacementService {

    private final OrderIdGenerator idGenerator;
    private final OrderStore orderStore;

    public OrderPlacementService(OrderIdGenerator idGenerator, OrderStore orderStore) {
        // Зависимости сервиса (bean-а) приходят извне — от контейнера
        this.idGenerator = idGenerator;
        this.orderStore = orderStore;
    }

    public Order placeOrder(CreateOrderCommand cmd) {
        // Сервис создаёт доменный объект: это «продукт работы», а не bean
        Order order = new Order(idGenerator.nextId(), cmd.customerId(), OrderStatus.NEW, Instant.now());

        // Используем инфраструктурный bean для сохранения
        orderStore.save(order);

        return order;
    }
}

Здесь есть очень показательный момент: сервис сам создаёт доменный объект. Это не повод делать Order bean-ом. Наоборот: это как раз показывает, что доменные объекты — продукт работы сервиса.

И теперь — кусок конфигурации, который показывает: контейнеру достаточно знать только про «механизмы»:

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

@Configuration // Конфигурация, которая описывает каркас приложения
public class AppConfig {

    @Bean // Инфраструктурный bean: генератор идентификаторов
    public OrderIdGenerator orderIdGenerator() {
        return new UuidOrderIdGenerator();
    }

    @Bean // Сервисный bean: контейнер сам «свяжет» его зависимости (gen, store)
    public OrderPlacementService orderPlacementService(OrderIdGenerator gen, OrderStore store) {
        return new OrderPlacementService(gen, store);
    }
}

В этом конфиге нет @Bean public Order () — и это отлично. Мы не пытаемся заставить контейнер «держать заказ». Контейнер хранит то, что нужно, чтобы заказы создавать и сохранять.

6. Правила выбора beans для новичка

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

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

Второй вопрос — «сколько таких объектов будет в ходе работы?». Если их потенциально много (заказы, позиции заказа, команды), контейнер тут не нужен: он не должен управлять тысячами однотипных экземпляров, которые рождаются каждую секунду. Если объект обычно один на приложение (хранилище, генератор id, сервис), bean — хороший вариант.

Третий вопрос — «нужны ли объекту данные, известные только во время выполнения?». Если объект нельзя создать без значений, которые появятся лишь в момент сценария (например: customerId, список OrderItem, createdAt), то это почти прямой сигнал, что перед вами не bean. Контейнер создаёт bean-ы во время старта приложения, а не в момент «пользователь решил купить ручку».

Чтобы не оставаться в абстракции, посмотрим на плохой, но показательный, пример. Вот так делать не надо:

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

@Configuration // Это выглядит «как конфиг», но на самом деле конфигурирует доменные данные — так не надо
public class BrokenConfig {

    @Bean // Антипаттерн: делаем доменную сущность bean-ом и ещё и с захардкоженными данными
    public Order order() {
        return new Order("o-1", "cust-1", OrderStatus.NEW, java.time.Instant.now());
    }
}

Почему это плохая идея даже без тонкостей Spring:

Такой Order превращается в «структуру приложения», хотя должен быть «данными». Он создаётся при старте контекста — ещё до того, как мы вообще решили оформить заказ. Экземпляр будет один, хотя заказов нам нужно много. И вдобавок вы фактически захардкодили customerId, а Instant.now() на старте приложения — это вообще отдельный вид сюрреализма: заказ «создан» в момент запуска программы, даже если пользователь ещё ничего не делал.

Хороший «контрпример» — сделать bean-ом не Order, а сервис, который умеет его создавать, и зависимости, которые сервису нужны. Тогда система остаётся естественной: Spring управляет механизмами, а бизнес-сценарий управляет данными.

Чтобы это было проще запомнить, можно смотреть на container boundary как на простое правило: «внутри контейнера — то, что помогает работать», «снаружи — то, над чем мы работаем».

7. Типичные ошибки при выборе bean-ов

Ошибка №1: пытаться сделать bean-ом каждую сущность домена («пусть Spring заведёт мне Order-ы»).
Обычно это происходит из желания «использовать Spring по максимуму». На практике вы получаете странную конфигурацию, где доменные данные становятся частью стартовой структуры приложения, а потом ещё и пытаются жить как один общий экземпляр. Доменная сущность — это данные конкретного сценария, и ей нормально появляться через new внутри сервисов или сценарных классов.

Ошибка №2: держать временные данные сценария внутри bean-а как поле класса.
Это выглядит невинно: «ну я сохраню последний созданный заказ в поле, вдруг пригодится». Но очень быстро это превращается в скрытое состояние, которое не относится к «механизму». Bean должен быть «сервисом», а не «папкой с текущими документами». Временные данные лучше держать в локальных переменных методов, в возвращаемых объектах или в явно выделенных структурах хранения (например, в OrderStore).

Ошибка №3: путать «важный объект» и «bean».
Order важнее любого OrderPlacementService по смыслу предметной области, но это не делает его bean-ом. Bean-ом объект становится не потому, что он важный, а потому, что он часть каркаса приложения: его нужно собрать, связать с зависимостями и поддерживать в рабочем состоянии.

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

Ошибка №5: считать, что «если объект не bean, то Spring не нужен».
Иногда студенты после разделения на beans и обычные объекты думают: «Так у меня половина объектов создаётся через new, значит Spring не особо помог». Это ловушка. Spring берёт на себя самое неприятное: сборку и сопровождение объектного графа сервисов и инфраструктуры. А то, что доменные объекты создаются вручную, — не недостаток, а нормальная, здоровая модель приложения.

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