JavaRush /Курсы /Spring Core /@Bean для инфрастру...

@Bean для инфраструктуры и настроек

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

1. Роль @Bean рядом со scanning

Ограничение scanning видно почти сразу: он отлично находит наши @Service и @Component, но не отвечает на вопрос, как положить в контейнер Clock, DateTimeFormatter или аккуратно настроенный объект из сторонней библиотеки. Здесь уже мало сказать Spring’у «найди подходящий класс в пакетах». Ему нужно дать правило создания.

Именно это и делает @Bean: не ищет кандидата, а явно говорит контейнеру, какой объект должен появиться и как его собрать. Scanning и @Bean не конкурируют. Scanning хорош для обычных компонентов проекта, а @Bean закрывает точечные места, где важны чужой тип, настройка или явный выбор реализации.

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

Чтобы это почувствовать на пальцах, можно представить два подхода как два режима управления светом в комнате. Scanning — это датчик движения: удобно, быстро, почти всегда работает. @Bean — это выключатель, который вы ставите там, где датчик начинает чудить (например, в коридоре он включается от кота, а вам надо строго от человека).

2. Что регистрирует @Bean

Когда вы впервые видите @Bean, легко подумать, что Spring «регистрирует метод». Но на самом деле Spring регистрирует объект, который вернёт этот метод, а метод — это лишь фабричный способ сказать контейнеру, как объект создать. Это важное отличие, потому что дальше мы будем мыслить не «методами», а именно bean-ами: именами, типами, жизнью внутри контейнера.

Самый минимальный пример — положить в контейнер Clock. Это класс из JDK, в котором мы точно не будем писать @Component (да и не можем — он не наш).

import java.time.Clock;

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

@Configuration
public class TimeConfig {

    @Bean
    Clock clock() {
        // Регистрируем "время" как инфраструктурную зависимость:
        // теперь его можно внедрять в любые классы через DI.
        return Clock.systemUTC();
    }
}

Здесь есть сразу три практических момента.

Во-первых, метод clock() становится bean-методом: он участвует в Java config и возвращает bean.

Во-вторых, имя bean-а по умолчанию берётся из имени метода. То есть bean будет называться "clock".

В-третьих, возвращаемый тип не обязан быть «вашим», а сам код создания — обычный Java. И это прекрасно: никакой магии, всё читается как фабрика.

Проверим, что контейнер действительно знает про этот bean:

import java.time.Clock;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApp {
    public static void main(String[] args) {
        // Поднимаем контекст только из одного @Configuration-класса
        try (var context = new AnnotationConfigApplicationContext(TimeConfig.class)) {
            // Достаём bean по имени и типу (для демонстрации правила нейминга)
            Clock clock = context.getBean("clock", Clock.class);

            // Проверяем, что это UTC-часы
            System.out.println(clock.getZone()); // Z
        }
    }
}

Заметьте: мы достали bean по имени "clock", потому что это имя метода. В реальном проекте вы чаще будете доставать по типу, но для понимания «откуда берётся имя» полезно один раз сделать именно так.

Иногда имя нужно сделать явным, чтобы оно не зависело от того, как вы назвали метод (или чтобы читалось лучше в wiring-ошибках). Тогда можно дать имя прямо в аннотации:

import java.time.Clock;

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

@Configuration
public class TimeConfig {

    @Bean("utcClock")
    Clock clock() {
        // Явно фиксируем имя bean-а, чтобы оно было стабильным и понятным
        return Clock.systemUTC();
    }
}

Теперь bean называется "utcClock". А метод всё ещё может называться clock(). Это выглядит странно, если злоупотреблять, но как инструмент для читаемости — полезно.

Для ориентира сведём naming-правило в маленькую табличку:

Как написано Какое имя bean-а получится
@Bean Clock clock() clock
@Bean("utcClock") Clock clock() utcClock

И да, именно поэтому метод в конфиге стоит называть как «роль объекта в контейнере», а не как «какое-то случайное слово». Называть bean-метод a() — технически можно, но это будет очень честный способ сказать будущему себе: «я не люблю себя».

3. @Bean для сторонних классов

Очень часто @Bean в реальной жизни начинается со сторонних классов. И речь не только о «какой-то библиотеке из Maven», а даже о JDK. Если вы пишете non-web приложение (как наш ContextFlow) и хотите аккуратно управлять временем, датами, форматированием, путями к файлам, у вас появляется куча объектов, которые хочется иметь едиными, настраиваемыми и под контролем контейнера.

Например, нам может понадобиться единый DateTimeFormatter для аудита и отчётов (да, даже в консольном приложении люди любят, когда даты выглядят одинаково).

import java.time.format.DateTimeFormatter;

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

@Configuration
public class FormattingConfig {

    @Bean
    DateTimeFormatter auditDateTimeFormatter() {
        // Централизуем форматирование дат: один формат на всё приложение
        return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    }
}

Важная мысль: мы не обязаны «раскидывать форматтер по коду» через new там, где он нужен. Мы можем дать контейнеру один объект и затем внедрять его куда надо через DI.

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

Похожий пример — java.nio.file.Path. В нашем проекте по требованиям все артефакты должны писаться в build/, и это отличное место, чтобы зафиксировать базовый output-путь как bean:

import java.nio.file.Path;

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

@Configuration
public class OutputConfig {

    @Bean
    Path reportsOutputDir() {
        // Фиксируем базовую директорию для отчётов в одном месте
        return Path.of("build", "contextflow", "reports");
    }
}

Это всё ещё «просто Java», но вы уже видите два выигрыша: объект централизован и его проще менять, плюс его можно переиспользовать в разных инфраструктурных классах без ручного прокидывания.

4. @Bean для инфраструктурных объектов

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

Возьмём наш ContextFlow. У нас есть контракт генерации идентификаторов заказа:

package com.example.contextflow.domain.ports;

public interface OrderIdGenerator {
    // Контракт: внешний мир хочет "следующий id", не вникая в реализацию
    String nextId();
}

И есть реализация, которую мы хотим использовать по умолчанию:

package com.example.contextflow.infrastructure.id;

import java.util.UUID;

import com.example.contextflow.domain.ports.OrderIdGenerator;

public class UuidOrderIdGenerator implements OrderIdGenerator {
    @Override
    public String nextId() {
        // Выбранная стратегия генерации: UUID (решение инфраструктурного уровня)
        return UUID.randomUUID().toString();
    }
}

Если мы сделаем UuidOrderIdGenerator компонентом, то формально всё будет работать. Но как только вы читаете проект, у вас возникает вопрос: «А почему именно UUID? А где это решается?». На уровне домена это решение не обязано жить, а вот на уровне сборки приложения — вполне.

Тогда конфигурация выглядит так:

import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.infrastructure.id.UuidOrderIdGenerator;

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

@Configuration
public class IdConfig {

    @Bean
    OrderIdGenerator orderIdGenerator() {
        // Возвращаем интерфейс, но создаём конкретную реализацию:
        // это упрощает замену стратегии в будущем.
        return new UuidOrderIdGenerator();
    }
}

Здесь важны две вещи.

Первая: метод возвращает интерфейс (OrderIdGenerator), а создаёт конкретную реализацию. Это «маленькая, но очень взрослая» привычка. Она помогает не привязывать остальной код к реализации.

Вторая: решение «в этом варианте приложения используем UUID» лежит в месте, где ему логично лежать: в конфигурации, рядом с wiring.

А ещё @Bean удобно использовать для объектов, которые вы считаете инфраструктурой и не хотите, чтобы они «наравне» стояли в списке компонентов. Например, менеджер вывода отчётов (ReportOutputManager) может быть просто классом, который принимает Path и обеспечивает запись в правильную директорию. Мы не обязаны превращать каждую такую штуку в @Component, особенно если она создаётся с параметрами.

5. Параметры @Bean-метода

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

Представим, что мы хотим аудит, который пишет сообщения с timestamp. Пусть у нас есть класс-обёртка, которому нужен Clock:

package com.example.contextflow.infrastructure.audit;

import java.time.Clock;
import java.time.Instant;

import com.example.contextflow.domain.ports.AuditWriter;

public class TimeStampedAuditWriter implements AuditWriter {
    private final Clock clock;
    private final AuditWriter delegate;

    public TimeStampedAuditWriter(Clock clock, AuditWriter delegate) {
        // Clock используем для получения "текущего времени" (и для тестируемости)
        this.clock = clock;
        // Delegate делает реальную запись (в консоль/файл и т.д.)
        this.delegate = delegate;
    }

    @Override
    public void write(String message) {
        // Добавляем timestamp и прокидываем дальше в реального writer-а
        delegate.write(Instant.now(clock) + " | " + message);
    }
}

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

Теперь вопрос: как его зарегистрировать? Если сделать @Component, нам придётся думать про то, как выбирать делегата и как передавать Clock. А через @Bean это читается как «собрать объект из частей»:

import java.time.Clock;

import com.example.contextflow.domain.ports.AuditWriter;
import com.example.contextflow.infrastructure.audit.ConsoleAuditWriter;
import com.example.contextflow.infrastructure.audit.TimeStampedAuditWriter;

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

@Configuration
public class AuditConfig {

    @Bean
    Clock clock() {
        // Единый источник времени для приложения
        return Clock.systemUTC();
    }

    @Bean
    AuditWriter auditWriter(Clock clock) {
        // Spring сам подставит Clock из контекста в параметр метода
        return new TimeStampedAuditWriter(clock, new ConsoleAuditWriter());
    }
}

И вот тут появляется «легальный следующий вопрос»: а почему ConsoleAuditWriter создаётся через new, а не тоже как bean? Отличный вопрос — и он как раз подводит к правильной привычке: если контейнер может дать зависимость, пусть даёт. Тогда конфиг становится ещё чище:

import java.time.Clock;

import com.example.contextflow.domain.ports.AuditWriter;
import com.example.contextflow.infrastructure.audit.ConsoleAuditWriter;
import com.example.contextflow.infrastructure.audit.TimeStampedAuditWriter;

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

@Configuration
public class AuditConfig {

    @Bean
    Clock clock() {
        // Единый Clock в контейнере (удобно менять и удобно тестировать)
        return Clock.systemUTC();
    }

    @Bean
    AuditWriter consoleAuditWriter() {
        // Отдельный bean для "реального" writer-а (консоль в нашем примере)
        return new ConsoleAuditWriter();
    }

    @Bean
    AuditWriter auditWriter(Clock clock, AuditWriter consoleAuditWriter) {
        // Собираем декоратор из зависимостей, которые дал контейнер
        return new TimeStampedAuditWriter(clock, consoleAuditWriter);
    }
}

Да, тут может смутить параметр AuditWriter consoleAuditWriter: «а как Spring поймёт, что это именно тот AuditWriter, а не другой?». Здесь контейнер может однозначно связать зависимость с bean-ом consoleAuditWriter по имени параметра, и нам этого достаточно. Сейчас важно увидеть сам принцип: зависимость приходит из контейнера, а не собирается вложенными new.

Важно, что параметры @Bean-метода — это не «передача параметров в Java». Это часть контейнерной модели: Spring подставляет значения сам, и вы не обязаны вызывать context.getBean() руками. И это хорошая новость, потому что getBean() внутри бизнес-кода быстро приводит к service locator-стилю (а мы от него как раз уходили ещё на истории про composition root).

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

flowchart TD
    A["@Configuration class"] --> B["@Bean method: auditWriter(Clock, AuditWriter)"]
    B --> C["Spring resolves parameters by type"]
    C --> D["Spring calls method once (singleton default)"]
    D --> E["Returned object becomes a bean in context"]

Это не «магия вызова методов», а обычная фабрика, где контейнер берёт на себя поиск зависимостей и порядок создания.

6. Практика: Clock и id в конфиге

На этом месте полезно собрать самый короткий рабочий гибрид. Бизнес-слой оставляем на scanning, а через @Bean добавляем только несколько инфраструктурных зависимостей. Это ещё не окончательная раскладка конфигов ContextFlow; здесь нам важен сам механизм: как scanning и Java config уживаются в одном контексте.

Предположим, что у нас уже есть ScenarioRunner, и он зарегистрирован через stereotype-аннотацию, например @Component. Тогда мы можем поднять контекст из двух источников: scanning найдёт ScenarioRunner, а @Bean даст ему нужные инфраструктурные зависимости.

Сделаем корневой конфиг в пакете com.example.contextflow.config.core и сознательно оставим его коротким: сейчас нам важен принцип гибридной сборки, а не финальная тонкая декомпозиция.

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

@Configuration
@ComponentScan(basePackages = {
        "com.example.contextflow.application",
        "com.example.contextflow.infrastructure"
})
public class CoreConfig {
}

Пакет config здесь не сканируем: явные конфиги подключаем руками, иначе первая гибридная сборка быстро становится мутной.

Теперь отдельным конфигом зарегистрируем то, что не хотим/не можем сканировать:

import java.time.Clock;

import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.infrastructure.id.UuidOrderIdGenerator;

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

@Configuration
public class InfrastructureConfig {

    @Bean
    Clock clock() {
        return Clock.systemUTC();
    }

    @Bean
    OrderIdGenerator orderIdGenerator() {
        return new UuidOrderIdGenerator();
    }
}

Здесь InfrastructureConfig играет роль общего контейнера для явных @Bean-регистраций. Если явной инфраструктуры станет больше, конфиги можно будет разложить тоньше; пока это только мешало бы увидеть сам механизм.

А теперь старт приложения — честный AnnotationConfigApplicationContext, без Boot:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowApp {
    public static void main(String[] args) {
        try (var context = new AnnotationConfigApplicationContext(
                CoreConfig.class,
                InfrastructureConfig.class
        )) {
            var runner = context.getBean("scenarioRunner");
            System.out.println("Runner bean = " + runner.getClass().getSimpleName());
            // Runner bean = ScenarioRunner
        }
    }
}

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

Что важно в этой мини-практике: мы не «переписали проект на @Bean». Мы добавили инфраструктуру точечно, оставив бизнес-слой на scanning. Для первого гибридного наброска этого достаточно: видно, что оба способа регистрации спокойно живут в одном контексте и не требуют переписывать всё подряд.

7. Типичные ошибки при использовании @Bean

Ошибка №1: воспринимать @Bean как “более правильный способ”, и пытаться переписать на него весь проект.
Это частая реакция после первого успеха с Java config: хочется всё контролировать, всё создавать руками и жить в конфиге. Обычно это приводит к гигантским конфигурационным классам, в которых вы теряете структуру приложения. Хороший ориентир на сегодня простой: бизнес-классы, которые естественно сканируются и не требуют настройки, спокойно остаются на stereotypes, а @Bean используйте там, где он реально даёт прозрачность или контроль.

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

Ошибка №3: создавать зависимости через вложенные new, когда контейнер уже может их передать параметрами.
Конфиг вида return new X(new Y(new Z())) выглядит как «мы вернулись в manual wiring, только внутри Spring». Приучайте себя к простому стилю: пусть зависимости приходят в параметры @Bean-метода. Так wiring остаётся читаемым, и Spring сам контролирует создание и reuse зависимостей.

Ошибка №4: забывать, что имя метода — это имя bean-а по умолчанию, и называть методы “как попало”.
Пока проект маленький, вы можете назвать метод a() и ничего не почувствовать. Но как только вы поймаете ошибку wiring и увидите в исключении список кандидатов “a, b, c”, вы поймёте, что сделали себе маленькую ловушку. Название bean-метода — это часть читабельности контейнера, особенно когда вы начинаете диагностировать проблемы.

Ошибка №5: дублировать регистрацию одного и того же объекта через scanning и через @Bean.
Иногда один и тот же класс помечают @Component, а затем ещё и возвращают из @Bean-метода. Контейнер не любит неопределённость: вы получите либо конфликт имён, либо два разных beans одного типа, а дальше — странные ошибки выбора кандидата. Договоритесь с собой: для каждого класса выбираем один понятный путь регистрации. Если уж держим объект в Java config — значит он там и живёт, без “двойной прописки”.

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