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

@Bean для інфраструктури й налаштувань

Spring Core
Рівень 6 , Лекція 1
Відкрита

1. Роль @Bean поруч зі скануванням

Обмеження сканування видно майже одразу: воно чудово знаходить наші @Service і @Component, але не відповідає на питання, як покласти в контейнер Clock, DateTimeFormatter або акуратно налаштований обʼєкт зі сторонньої бібліотеки. Тут уже замало сказати Springʼу: «знайди підходящий клас у пакетах». Йому потрібно дати правило створення.

Саме це і робить @Bean: він не шукає кандидата, а явно говорить контейнеру, який обʼєкт має зʼявитися і як його зібрати. Сканування й @Bean не конкурують. Сканування добре підходить для звичайних компонентів проєкту, а @Bean закриває точкові місця, де важливі чужий тип, налаштування або явний вибір реалізації.

Іноді обʼєкт приходить із JDK або сторонньої бібліотеки. Іноді ви хочете повернути інтерфейс, але створити конкретну реалізацію. Іноді важливе точне налаштування в момент створення: один параметр — і обʼєкт поводиться інакше. У таких місцях @Bean стає чесним і прозорим інструментом: «контейнере, ось тобі правило — створи обʼєкт ось так».

Щоб відчути це на практиці, уявіть два режими керування світлом у кімнаті. Сканування — це датчик руху: зручно, швидко, майже завжди працює. @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", бо це імʼя методу. У реальному проєкті ви частіше діставатимете його за типом, але для розуміння, звідки береться імʼя, корисно один раз зробити саме так.

Іноді імʼя потрібно зробити явним, щоб воно не залежало від того, як ви назвали метод, або щоб краще читалося в повідомленнях про помилки збирання. Тоді можна дати імʼя прямо в анотації:

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(). Це виглядає дивно, якщо зловживати, але як інструмент для читабельності — корисно.

Для орієнтиру зведімо правило іменування в маленьку табличку:

Як написано Яке імʼя bean-а вийде
@Bean Clock clock() clock
@Bean("utcClock") Clock clock() utcClock

І саме тому метод у конфігу варто називати за роллю обʼєкта в контейнері, а не випадковим словом. Назвати bean-метод a() — технічно можна, але це буде надто чесний спосіб сказати собі: «я не люблю себе».

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

Дуже часто @Bean у реальному житті починається зі сторонніх класів. Ідеться не лише про «якусь бібліотеку з Maven», а навіть про JDK. Якщо ви пишете застосунок без веб-частини (як наш 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/, і це гарне місце, щоб зафіксувати базовий шлях виводу як 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» лежить саме там, де йому місце: у конфігурації, поруч зі збиранням.

А ще @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;
        // Делегат робить реальний запис (у консоль, файл тощо)
        this.delegate = delegate;
    }

    @Override
    public void write(String message) {
        // Додаємо мітку часу й передаємо далі справжньому 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 підбирає параметри за типом"]
    C --> D["Spring викликає метод один раз (singleton за замовчуванням)"]
    D --> E["Повернений об'єкт стає bean-ом у контексті"]

Це не «магія виклику методів», а звичайна фабрика, де контейнер бере на себе пошук залежностей і порядок створення.

6. Практика: Clock і id у конфігурації

На цьому місці корисно зібрати найкоротший робочий гібрид. Бізнес-рівень залишаємо на скануванні, а через @Bean додаємо лише кілька інфраструктурних залежностей. Це ще не остаточне розкладання конфігів ContextFlow; нам важливий сам механізм: як сканування і Java config уживаються в одному контексті.

Припустімо, що в нас уже є ScenarioRunner, і він зареєстрований через стереотипну анотацію, наприклад @Component. Тоді ми можемо підняти контекст із двох джерел: сканування знайде 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("Bean runner = " + runner.getClass().getSimpleName());
            // Bean runner = ScenarioRunner
        }
    }
}

Тут я свідомо показав отримання bean-а за рядковим іменем, бо це добре повʼязується з темою «імʼя bean-а і звідки воно береться». У реальному коді ви частіше діставатимете його за типом і, ще частіше, взагалі не діставатимете вручну, а просто запускатимете «головний сценарій» з одного відомого bean-а.

Що важливо в цій мініпрактиці: ми не «переписали проєкт на @Bean». Ми точково додали інфраструктуру, залишивши бізнес-рівень на скануванні. Для першого гібридного ескізу цього досить: видно, що обидва способи реєстрації спокійно живуть в одному контексті й не вимагають переписувати все підряд.

7. Типові помилки під час використання @Bean

Помилка №1: сприймати @Bean як «правильніший спосіб» і намагатися переписати на нього весь проєкт.
Це часта реакція після першого успіху з Java config: хочеться все контролювати, усе створювати вручну і жити в конфігу. Зазвичай це приводить до гігантських конфігураційних класів, у яких ви втрачаєте структуру застосунку. Хороший орієнтир на сьогодні простий: бізнес-класи, які природно скануються і не потребують налаштування, спокійно залишаються на стереотипних анотаціях, а @Bean використовуйте там, де він справді дає прозорість або контроль.

Помилка №2: робити в @Bean-методах «життя», а не «створення».
Bean-метод має відповідати на питання «як створити обʼєкт», а не «що зробити під час запуску застосунку». Якщо всередині @Bean ви друкуєте банери, читаєте файли, ходите мережею, створюєте тимчасові дані «для сценарію» — ви змішуєте збирання і виконання програми. На перших кроках це може спрацювати, але потім такі сайд-ефекти перетворюються на несподіванки під час старту, тестування й налагодження.

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

Помилка №4: забувати, що імʼя методу — це імʼя bean-а за замовчуванням, і називати методи як завгодно.
Поки проєкт маленький, ви можете назвати метод a() і нічого не відчути. Але щойно ви спіймаєте помилку збирання й побачите у винятку список кандидатів «a, b, c», ви зрозумієте, що створили собі маленьку пастку. Назва bean-методу — це частина читабельності контейнера, особливо коли ви починаєте діагностувати проблеми.

Помилка №5: дублювати реєстрацію одного й того самого обʼєкта через сканування і через @Bean.
Іноді один і той самий клас позначають @Component, а потім ще й повертають із @Bean-методу. Контейнер не любить невизначеності: ви отримаєте або конфлікт імен, або два різні bean-и одного типу, а далі — дивні помилки вибору кандидата. Домовтеся із собою: для кожного класу обираємо один зрозумілий шлях реєстрації. Якщо вже тримаємо обʼєкт у Java config — значить, він там і живе, без «подвійної прописки».

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