1. Сканування та Java-конфігурація в ContextFlow
До цього моменту в проєкті вже є всі потрібні ролі. Сканування компонентів знаходить звичайні компоненти, @Configuration дає точку збирання, @Bean реєструє інфраструктуру, а параметри @Bean-методів не дають збиранню перетворюватися на прихований new всередині конфігурації. Залишилося зібрати це в один зрозумілий ContextFlow, щоб у застосунку був читабельний гібридний зріз, а не набір розрізнених фрагментів.
Але реальний проєкт майже одразу впирається в незручний факт: не всі корисні обʼєкти виглядають як «кандидати на посаду», яких шукають за classpath. У ContextFlow починають зʼявлятися генератори ідентифікаторів, форматувачі звітів, менеджер виведення файлів, Clock, Path та інша інфраструктура. Такі речі часто простіше й чесніше зібрати явно: не тому, що Spring «не вміє», а тому, що ви хочете бачити рішення в одному місці й керувати ним усвідомлено.
Щоб не бути голослівними, зафіксуємо думку у вигляді короткої схеми:
flowchart TD
A[компонентне сканування] --> B["@Service / @Repository класи"]
C["@Configuration + @Bean"] --> D["інфраструктурні та сторонні обʼєкти"]
B --> E[ApplicationContext]
D --> E[ApplicationContext]
Тут ключове: сканування відповідає на запитання «які класи вважаємо компонентами системи», а Java-конфігурація — на запитання «якими саме реалізаціями і з якими налаштуваннями ми цю систему збираємо».
Гібридне збирання працює добре доти, доки ви тримаєте просте правило: один бін — одне джерело реєстрації. Тоді сканування відповідає за звичайні компоненти, а Java-конфігурація — за явну інфраструктуру, і контейнер не перетворюється на лотерею.
2. Критерії: сканування і @Bean у ContextFlow
Коли новачок уперше бачить @Bean, виникає спокуса: «А давайте тепер усе через @Bean — так само явно!». Потім зʼявляється інша спокуса: «А давайте все через scanning — так само зручно!». Обидві зрозумілі, і обидві зазвичай закінчуються тим, що за кілька тижнів ви самі себе вже не розумієте. У реальному житті перемагає гібрид, де кожен інструмент працює за призначенням.
Нижче — орієнтовна карта для нашого ContextFlow. Це не «закон Spring», а практичний критерій для навчального проєкту: де сканування справді робить код читабельнішим, а де явний @Bean знімає зайві запитання.
| Обʼєкт у проєкті | Як реєструємо | Чому це читається краще |
|---|---|---|
OrderPlacementService, , |
сканування (@Service) | Це бізнес-оркестратори: їх зручно «знаходити» за пакетом і роллю, а живуть вони логічно в application.* |
| InMemoryOrderStore | сканування (@Repository) | Це інфраструктурна реалізація порту, але все ще «наш клас», без особливих налаштувань; сканування тут підходить |
| OrderIdGenerator (конкретна реалізація) | @Bean | Вибір реалізації — це рішення під час збирання застосунку, а не властивість класу |
| ReportFormatter (конкретний форматувач) | @Bean | Часто потребує вибору реалізації та налаштування, а ще зручно тримати це поряд зі звітністю |
ReportOutputManager, для каталогу |
@Bean | Це чиста інфраструктура; її зручніше збирати явно, щоб одразу бачити шлях build/... |
Ще одна тонкість, яку варто проговорити: доменні обʼєкти (Order, Customer, OrderItem) ми взагалі не тягнемо в контейнер. Вони створюються під час сценарію, приходять із команд і живуть як звичайні обʼєкти. Контейнер — це про довгоживучі «деталі машини», а не про кожну гайку, яку ви сьогодні прикрутили.
3. Мініінфраструктура прикладу
Щоб конфігурація не виглядала як магічний «палець у небо @Bean», нам потрібні хоча б три прості інфраструктурні контракти. Ми не будемо ускладнювати домен: просто зробимо невеликі класи, які легко читати й приємно конфігурувати. Це схоже на ситуацію, коли ви збираєте набір LEGO: самі деталі прості, але важливі порядок і те, які деталі ви взагалі кладете в коробку.
Почнімо з генератора ідентифікаторів замовлення — це ідеальний кандидат для Java-конфігурації, тому що сама сутність «генератор» — інфраструктурна, а вибір реалізації — рішення під час збирання.
package com.example.contextflow.domain.ports;
public interface OrderIdGenerator {
// Контракт інфраструктури: сервісам важливо "отримати наступний ID",
// а як саме він генерується — вирішує збирання застосунку.
String nextId();
}
І проста реалізація без анотацій (важливо: без @Component, ми реєструватимемо її через @Bean):
package com.example.contextflow.infrastructure.id;
import com.example.contextflow.domain.ports.OrderIdGenerator;
import java.util.UUID;
public class UuidOrderIdGenerator implements OrderIdGenerator {
@Override
public String nextId() {
// UUID добрий тим, що не потребує зовнішнього стану і легко тестується.
return UUID.randomUUID().toString();
}
}
Тепер звітність. На цьому етапі достатньо розуміти, що форматувач перетворює модель звіту на рядок, а менеджер виведення відповідає за те, куди цей рядок зберегти або хоча б як обчислити шлях. Ось мінімальні контракти:
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
public interface ReportFormatter {
// Інфраструктурний порт: форматування звіту — це "як подати дані", а не бізнес-логіка.
String format(DailyReport report);
}
Простіша реалізація форматувача:
package com.example.contextflow.infrastructure.reporting;
import com.example.contextflow.domain.model.DailyReport;
public class TextReportFormatter implements ReportFormatter {
@Override
public String format(DailyReport report) {
// Мінімальний навчальний формат, щоб було видно, що звіт "зібрався".
return "orders=" + report.orderCount();
}
}
І «менеджер виведення» — обʼєкт, який знає каталог і допомагає будувати шлях. Він максимально нудний, а це для інфраструктури — комплімент.
package com.example.contextflow.infrastructure.reporting;
import java.nio.file.Path;
public class ReportOutputManager {
private final Path outputDir;
public ReportOutputManager(Path outputDir) {
// Каталог виводиться назовні як залежність — це зручно налаштовувати через конфіг.
this.outputDir = outputDir;
}
public Path resolve(String fileName) {
// Тут бізнес-логіки немає: просто збирання підсумкового шляху.
return outputDir.resolve(fileName);
}
}
Зверніть увагу на приємний ефект: жоден із цих класів не знає про Spring. Це звичайні Java-обʼєкти. Саме цього ми й прагнемо: Spring має бути «клеєм збирання», а не обовʼязковою частиною кожного класу.
4. Конфігурація: CoreConfig і ReportingConfig
Тепер зберемо робочу конфігурацію ContextFlow, на яку вже зручно спиратися далі: CoreConfig тримає межі сканування й базову інфраструктуру, а ReportingConfig — явну звітну частину. Це короткий поділ, але він уже не ховає рішення про збирання.
Почнемо з CoreConfig. Він задає межі сканування і реєструє OrderIdGenerator.
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.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ComponentScan(basePackages = {
"com.example.contextflow.application",
"com.example.contextflow.infrastructure"
})
public class CoreConfig {
@Bean
public OrderIdGenerator orderIdGenerator() {
// Вибір реалізації — частина збирання застосунку.
return new UuidOrderIdGenerator();
}
}
Тут важливі дві речі: пакет config ми не скануємо, а підключаємо явно, і вибір UuidOrderIdGenerator залишається рішенням під час збирання застосунку, а не властивістю самого класу.
Тепер ReportingConfig. Тут тримаємо явну інфраструктуру звітності: каталог виведення, менеджер виведення та форматувач.
import com.example.contextflow.infrastructure.reporting.ReportFormatter;
import com.example.contextflow.infrastructure.reporting.ReportOutputManager;
import com.example.contextflow.infrastructure.reporting.TextReportFormatter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.nio.file.Path;
@Configuration(proxyBeanMethods = false)
public class ReportingConfig {
@Bean
public Path reportOutputDir() {
// Поки шлях фіксований: тут нам важлива сама наявність явної інфраструктурної точки.
return Path.of("build/reports");
}
@Bean
public ReportOutputManager reportOutputManager(Path reportOutputDir) {
// Залежність приходить параметром, тому звʼязування залишається передбачуваним і в режимі lite.
return new ReportOutputManager(reportOutputDir);
}
@Bean
public ReportFormatter reportFormatter() {
// Конкретний формат звіту обираємо на етапі збирання застосунку.
return new TextReportFormatter();
}
}
Цієї схеми вже досить, щоб побачити межу: сканування піднімає бізнес-складник, а @Bean закриває явну інфраструктуру звітності.
Тепер старт застосунку — через один AnnotationConfigApplicationContext, якому передано обидві конфігурації:
import com.example.contextflow.application.scenario.ScenarioRunner;
import com.example.contextflow.config.core.CoreConfig;
import com.example.contextflow.config.reporting.ReportingConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ContextFlowApplication {
public static void main(String[] args) {
// Контекст створюємо явно з двох конфігурацій: так видно, з чого зібрався застосунок.
try (var context = new AnnotationConfigApplicationContext(
CoreConfig.class,
ReportingConfig.class
)) {
// Запускаємо сценарій із контейнера: отже, залежності успішно зібралися.
context.getBean(ScenarioRunner.class).run();
}
}
}
На цьому етапі в проєкті вже є читабельна робоча конфігурація: CoreConfig відповідає за загальну рамку й базову інфраструктуру, ReportingConfig — за явні звітні деталі. Цього досить, щоб гібридна модель не перетворювалася на хаос і водночас не розросталася в десяток конфігів завчасно.
5. Діагностика дублікатів бінів
Щойно така розкладка стає явною, видно й головну небезпеку гібридного збирання: дубль одного й того самого біна. Spring чемно цього не пробачить — і це добре, бо інакше ви ловили б баги не на старті, а в проді, що зазвичай дорожче й емоційніше.
Найчастіший навчальний дубль виглядає так: ви залишили @Component на UuidOrderIdGenerator і водночас зареєстрували OrderIdGenerator через @Bean. У контейнері опиняться два біни, які підходять за типом OrderIdGenerator. А отже, будь-який сервіс, який просить OrderIdGenerator, отримає неоднозначність і падіння на старті.
Ось як виглядає той самий «випадковий дубль» (так робити не треба):
import org.springframework.stereotype.Component;
@Component // Помилка в стилі цієї лекції: клас уже реєструється через @Bean у конфігурації.
public class UuidOrderIdGenerator implements OrderIdGenerator {
// ...
}
У цьому місці корисно згадати діагностику з Дня 4: типова помилка буде в дусі NoUniqueBeanDefinitionException або у складі UnsatisfiedDependencyException. І головне: вона трапляється на старті, ще до виконання бізнес-коду. Тобто Spring буквально каже: «друже, я не телепат, обери одну стратегію збирання».
Швидка діагностика для себе, особливо в навчальному проєкті, — подивитися імена бінів за типом. Це не патерн для бізнес-коду, але як перевірка — чудово.
import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.config.core.CoreConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;
try (var ctx = new AnnotationConfigApplicationContext(CoreConfig.class)) {
// Діагностика: отримуємо всі імена бінів, які підходять за типом OrderIdGenerator.
System.out.println(Arrays.toString(ctx.getBeanNamesForType(OrderIdGenerator.class)));
// [orderIdGenerator]
}
Якщо ви побачите два імені в масиві, значить, десь створили дубль. Виправлення тут просте й чесне: обираєте одне джерело реєстрації. У нашому сьогоднішньому стилі генератор ID — це інфраструктура, тому логічніше залишити його без @Component і тримати вибір реалізації в конфігурації.
6. Типові помилки під час збирання інфраструктурної Java-конфігурації
Коли ви вперше починаєте збирати застосунок через конфіги, помилки зазвичай не про складні алгоритми. Вони про дисципліну: де що лежить, хто за що відповідає і наскільки ви випадково не перетворили @Configuration на другий сервісний шар. Нижче — найчастіші граблі саме для сьогоднішнього рівня, без переходу до тем наступних днів.
Помилка №1: «Давайте сканувати взагалі все, навіть config».
Якщо поставити @ComponentScan("com.example.contextflow"), контейнер почне підхоплювати й конфіги теж, тому що @Configuration — це теж компонент. Це не завжди погано, але на навчальному етапі часто породжує ефект «чому цей конфіг спрацював, я ж уже явно передав його в контекст?». Лікується дисципліною: скануйте application і infrastructure, а конфіги підключайте явно.
Помилка №2: дублювання реєстрації одного й того самого обʼєкта через scanning і @Bean.
Найпопулярніший сценарій — залишити @Component на класі, а потім ще додати @Bean «про всяк випадок». Контейнер отримає двох кандидатів одного типу й упаде з неоднозначністю на старті. Важливо не намагатися «вибирати» між ними костилями — на цьому етапі простіше й правильніше видалити одне з джерел реєстрації.
Помилка №3: proxyBeanMethods = false поставили, а потім почали викликати @Bean-методи один одного.
У режимі lite прямий виклик someBean() всередині іншого @Bean-методу стає звичайним Java-викликом, і ви легко створите зайвий обʼєкт, навіть якщо очікували singleton-поведінку. Якщо ви використовуєте режим lite, тримайте в голові просте правило: звʼязуйте біни через параметри @Bean-методів. Це і читається краще, і працює передбачувано.
Помилка №4: клас конфігурації починає робити побічні ефекти «прямо під час збирання».
Наприклад, ви всередині @Bean-методу друкуєте банери, створюєте файли, пишете щось у каталог, «перевіряєте мережу» і так далі. На старті застосунку це перетворюється на сюрпризи: контекст створюється — і раптом уже щось сталося. Конфіг має описувати створення обʼєктів, а не виконувати сценарій роботи застосунку.
Помилка №5: в одному конфігу опинилося «взагалі все».
Один гігантський AppConfig швидко перетворюється на звалище: там і звітність, і аудит, і генератори, і незрозумілі утиліти. У навчальному проєкті це особливо прикро: ви щойно зробили код читабельнішим завдяки скануванню, а потім знову все сховали в одну «конфіг-купу». Рішення просте: кілька класів конфігурації за відповідальністю та їхній явний перелік під час старту контексту.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ