1. Імена і типи в контейнері
Spring розрізняє біни не лише за типом, а й за імʼям. Поки застосунок невеликий, здається, що типу досить: «Дайте мені OrderPlacementService.class, будь ласка». Але щойно система бодай трохи ускладнюється, виявляється неприємне: один і той самий тип може траплятися кілька разів, а іноді потрібен саме конкретний екземпляр, а не «будь-який, що підходить». Саме тут і стають у пригоді імена.
Межу контейнера ми вже окреслили: не кожен обʼєкт застосунку зобовʼязаний бути біном. Тепер важливо зрозуміти, як контейнер розрізняє тих, хто все-таки опинився всередині.
Уявіть, що ApplicationContext — це велика шафа, а біни — коробки всередині неї. Тип — це як наліпка «інструменти» або «документи», а імʼя — уже конкретний підпис на коробці: «молоток», «пасатижі», «паспорт». За типом ви можете сказати: «Дайте будь-який інструмент», але іноді потрібен саме молоток, а не «що-небудь важке».
У Spring це працює так:
- пошук за типом — пошук за типом (клас/інтерфейс);
- пошук за імʼям — пошук за імʼям (рядок);
- і ще є alias — альтернативне імʼя для того ж біна.
Головна думка тут така: у повсякденному коді Spring намагається сам підставляти залежності через DI, а явний getBean(...) — це радше інструмент для точки входу та діагностики. Але навіть щоб діагностувати проблему, потрібно розуміти, як контейнер мислить: типами й іменами.
2. Bean name: імʼя обʼєкта в контейнері
Bean name — це рядковий ідентифікатор біна всередині контейнера. Він потрібен, щоб контейнер міг адресно сказати: «ось цей обʼєкт — scenarioRunner», «ось цей — orderStore». Усередині Spring це дуже схоже на словник: ключ → значення, де ключ — імʼя, а значення — бін (точніше, його визначення та/або екземпляр).
Важливо не переплутати: імʼя біна не зобовʼязане збігатися з іменем класу. Клас може називатися InMemoryOrderStore, а бін — orderStore. І це нормально: імʼя зазвичай відображає роль, а не технічну реалізацію. Роль у нас одна — «сховище замовлень», а реалізація може змінюватися (і пізніше змінюватиметься).
Є ще один момент, який новачка підловлює на першому повороті: імʼя — це не «просто для краси». Воно реально бере участь у житті контейнера. Якщо ви попросили context.getBean("orderStore"), Spring піде саме за імʼям. Якщо імʼя неправильне, контейнер не буде «здогадуватися», що ви мали на увазі «ну от той клас, який схожий». Він просто скаже: «не знайшов». І матиме рацію.
Щоб закріпити інтуїцію, зручно тримати в голові таку мінісхему:
flowchart LR
subgraph ApplicationContext["ApplicationContext (дуже спрощено)"]
N1["імʼя біна: orderStore"] --> D1["визначення біна"] --> I1["екземпляр: InMemoryOrderStore"]
N2["alias: mainOrderStore"] --> D1
end
T["пошук за типом: OrderStore"] --> N1
Схему навмисно спрощено, але ідея правильна: імʼя й alias ведуть до одного визначення, а тип — до кандидата або кандидатів, для яких цей тип підходить.
3. Імʼя біна в AppConfig
Зараз у нас ще немає component scanning, тому біни в проєкті ContextFlow ми реєструємо через конфігураційний клас (умовно назвемо його AppConfig). Тут діє дуже просте правило: якщо бін оголошено через @Bean-метод, то за замовчуванням імʼя біна дорівнює імені методу. Це не магія, а просто зручна домовленість.
Подивімося на невеликий шматок конфігурації. Вважайте, що це фрагмент усередині AppConfig:
import org.springframework.context.annotation.Bean;
// Важливо: метод позначено @Bean, отже Spring зареєструє результат як бін у контейнері.
// Тут імʼя біна за замовчуванням = імʼя методу (orderStore).
@Bean
public OrderStore orderStore() {
// Створюємо конкретну реалізацію, але назовні віддаємо як інтерфейс/абстракцію.
return new InMemoryOrderStore();
}
Імʼя біна тут буде "orderStore" — рівно як метод. Це зручно: ви читаєте конфіг як «у контейнері є orderStore», а не як «у контейнері лежить щось із довгою назвою реалізації».
Тепер — практичний момент. Щойно бін отримав імʼя, ви можете дістати його з контексту за імʼям:
// Дістаємо бін за імʼям і відразу перевіряємо тип (fail-fast).
OrderStore store = context.getBean("orderStore", OrderStore.class);
// Швидка перевірка того, яка реалізація лежить за інтерфейсом.
System.out.println(store.getClass().getSimpleName()); // InMemoryOrderStore
Зверніть увагу: ми використовуємо варіант getBean(name, type). Він безпечніший, ніж getBean(name) без типу, тому що Spring додатково перевірить: за цим імʼям справді лежить обʼєкт потрібного типу.
Іноді імʼя потрібно задати явно. Наприклад, метод може називатися orderStore(), але вам хочеться, щоб бін у контейнері називався mainOrderStore. Це робиться так:
import org.springframework.context.annotation.Bean;
// Явно задаємо імʼя біна (за замовчуванням імʼя методу вже НЕ використовуватиметься).
@Bean("mainOrderStore")
public OrderStore orderStore() {
return new InMemoryOrderStore();
}
Тепер пошук за "orderStore" уже не спрацює (якщо ви не зробили alias), а за "mainOrderStore" — спрацює. І ось тут у початківця зʼявляється важливе відчуття: «Ага, контейнер справді живе іменами, це не просто підписи в документації».
4. Alias: друга назва біна
Alias — це додаткова назва, яка вказує на той самий бін. Це не «другий бін» і не «другий екземпляр». Це просто «псевдонім» для вже наявного обʼєкта. Як у житті: одна людина, але в телефонній книзі вона може бути записана як «Андрій» і «Андрюха з DevOps».
У @Bean можна вказати одразу кілька імен. Перше вважається основним, решта — alias-ами. Наприклад, зробімо так для ScenarioRunner:
import org.springframework.context.annotation.Bean;
// Перше імʼя — основне, решта — alias.
// Усі імена вказуватимуть на одне й те саме BeanDefinition і (для singleton) на один екземпляр.
@Bean(name = {"scenarioRunner", "mainScenarioRunner"})
public ScenarioRunner scenarioRunner(OrderPlacementService service) {
// OrderPlacementService буде підставлено контейнером (DI), вручну тут його не шукаємо.
return new ScenarioRunner(service);
}
Тепер обидва імена ведуть до одного й того самого обʼєкта:
ScenarioRunner a = context.getBean("scenarioRunner", ScenarioRunner.class);
ScenarioRunner b = context.getBean("mainScenarioRunner", ScenarioRunner.class);
// Перевіряємо, що це один і той самий обʼєкт (а не дві різні копії).
System.out.println(a == b); // true
Це чудовий місток до наступної лекції про Spring singleton: ви буквально руками бачите, що в одному контейнері отримуєте один і той самий екземпляр. Alias тут лише підсилює спостережуваність: два різні ключі, а значення одне.
Навіщо alias взагалі потрібен? У реальних проєктах він трапляється, коли ви перейменовуєте бін (або мігруєте конфігурацію), але хочете зберегти зворотну сумісність: стара назва ще використовується десь у конфігурації або в старому коді, а нова вже «правильніша». Alias дозволяє зробити перехід мʼяким.
5. Пошук за типом: коли зручно і коли бурчить
Пошук за типом — найприємніший для мозку варіант: ви не думаєте про рядки, а думаєте про класи й інтерфейси. У невеликому застосунку це виглядає як «ну звісно, я хочу ScenarioRunner — він же один». І це справді нормальний підхід, поки кандидат рівно один.
Ось наш типовий bootstrap-код — фрагмент із main(), який дістає стартовий бін за типом:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
// Запускаємо контекст на основі конфігураційного класу.
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// Беремо бін за типом: працює, поки кандидат рівно один.
ScenarioRunner runner = context.getBean(ScenarioRunner.class);
// Запускаємо сценарій застосунку.
runner.run();
}
Плюс такого пошуку в тому, що він майже не залежить від імен. Ви можете перейменувати бін (або метод @Bean) — і код усе одно працюватиме, поки тип і кількість кандидатів не змінилися.
Але є й зворотний бік: пошук за типом працює лише тоді, коли Spring може вибрати однозначно. Якщо в контейнері опиниться два біни, що підходять під один інтерфейс, то getBean(Interface.class) перестане бути «зручним» і стане «контейнер бурчить».
Навіть до появи кількох реалізацій корисно знати простий діагностичний трюк: ApplicationContext уміє сказати, які імена зареєстровані для конкретного типу. Це хороший ліхтарик у темряві:
// Дивимося, які імена бінів підходять під указаний тип.
String[] names = context.getBeanNamesForType(OrderStore.class);
// Зручно виводити в діагностику, коли "за типом" стало неоднозначно.
System.out.println(String.join(", ", names)); // наприклад: orderStore
Поки OrderStore один, побачите одне імʼя. Коли кандидатів стане більше і контейнер почне бурчати, ви хоча б будете розуміти: «Ага, у мене справді два біни такого типу — ось їхні імена».
6. Пошук за імʼям
Пошук за імʼям потрібен тоді, коли ви хочете звернутися до біна як до конкретного екземпляра в контейнері, а не просто «до обʼєкта певного типу». Імена особливо корисні в діагностиці, на старті застосунку та в тих місцях, де ви свідомо керуєте вибором компонента.
Найпростіший варіант — getBean(String) — повертає Object. Він робочий, але новачка легко штовхає до зайвих кастів і помилок. Тому в навчальному проєкті краще одразу звикати до безпечнішої форми: getBean(String, Class).
Порівняйте два варіанти:
// Простий варіант: отримуємо Object (потім легко почати робити касти "на удачу").
Object raw = context.getBean("scenarioRunner");
// Швидка діагностика: що саме повернув контейнер (клас/проксі тощо).
System.out.println(raw.getClass().getSimpleName()); // ScenarioRunner (або проксі/обгортка пізніше)
і безпечніше:
// Безпечний варіант: Spring одразу перевірить, що за імʼям лежить потрібний тип.
ScenarioRunner runner = context.getBean("scenarioRunner", ScenarioRunner.class);
// Далі працюємо вже з типізованим посиланням.
runner.run(); // запускаємо сценарій
У другому варіанті Spring перевіряє тип просто на місці. Якщо ви помилилися імʼям або типом, отримаєте зрозумілу контейнерну помилку одразу, а не випадковий ClassCastException десь дорогою.
Важливо тримати межу: пошук за імʼям — це не «новий стиль писати бізнес-логіку». Це радше точковий інструмент, як викрутка. Нею зручно закрутити гвинтик, але незручно варити суп.
Пошук за імʼям + типом
Коли ви все-таки робите ручний пошук — наприклад, у main(), у ScenarioRunner як точці входу або в діагностиці, — найкраще триматися «золотої середини»: завжди вказувати і імʼя, і тип. Тоді код стає самодокументованим: одразу видно, який саме бін очікується і що ви хочете від нього отримати.
Ось короткий приклад, який одночасно показує три речі: імʼя, alias і безпеку типу:
// Беремо бін за alias (якщо alias зареєстровано) і водночас перевіряємо тип.
ScenarioRunner runner = context.getBean("mainScenarioRunner", ScenarioRunner.class);
System.out.println(runner.getClass().getSimpleName()); // ScenarioRunner
// Далі використовуємо як звичайний обʼєкт.
runner.run(); // запускаємо сценарій
Якщо alias не зареєстровано, контейнер чесно скаже, що біна з таким імʼям не знайдено. Якщо за цим імʼям лежить інший тип, контейнер так само чесно скаже, що тип не збігся. І вам не доведеться гадати, «чому воно впало десь далеко всередині».
У хорошому сенсі це схоже на замовлення в кавʼярні. Можна сказати «каву, будь ласка» (пошук за типом), а можна сказати «лате 300 мл на вівсяному» (пошук за імʼям + типом). Другий варіант займає на кілька секунд більше, але й шансів отримати не те менше.
7. Service Locator і getBean() у бізнес-логіці
У якийсь момент новачок робить відкриття: «О! Я можу будь-коли попросити контейнер видати мені що завгодно. Отже, можна не передавати залежності через конструктор». І ось тут DI починає тихо плакати в кутку, тому що ви непомітно приходите до антипатерну Service Locator.
Поганий приклад виглядає приблизно так — фрагмент, який краще ніколи не приносити на code review:
public class OrderPlacementService {
private final ApplicationContext context;
public OrderPlacementService(ApplicationContext context) {
// Залежність від контейнера: тепер клас "знає" про Spring і може діставати що завгодно.
this.context = context;
}
public void placeOrder(CreateOrderCommand cmd) {
// Прихована залежність: за сигнатурою методу і за полями класу не видно, що потрібен AuditWriter.
AuditWriter auditWriter = context.getBean(AuditWriter.class);
// Якась бізнес-логіка...
auditWriter.write("Замовлення створено");
}
}
Проблема не в тому, що Spring «забороняє так робити». Технічно це працює. Проблема в іншому: ви знову ховаєте залежності. За кодом класу більше не видно, що йому потрібен AuditWriter. Тестувати такий код складніше, змінювати реалізацію складніше, а помилки складання залежностей проявляться пізніше й у значно неприємнішому вигляді.
Гарний підхід — у дусі того, що ми вже робили на звичайній Java з DI, — тримати залежності як поля, передані ззовні, а не шукати їх на ходу:
public class OrderPlacementService {
private final AuditWriter auditWriter;
public OrderPlacementService(AuditWriter auditWriter) {
// Явна залежність: одразу видно, що сервісу потрібен AuditWriter.
this.auditWriter = auditWriter;
}
}
Контейнер Spring якраз для цього і потрібен: зібрати граф залежностей заздалегідь, а не для того, щоб ви «ловили» залежності посеред бізнес-методу, як покемонів у траві.
8. Діагностика: біни в контексті
Коли Spring падає під час старту або ви не розумієте, чому getBean(...) не знаходить потрібний обʼєкт, дуже хочеться хоч якось «подивитися всередину контейнера». Гарна новина: для навчальних цілей і діагностики це можна робити спокійно, не перетворюючи застосунок на контейнерозалежну кашу.
Найпростіший спосіб — вивести імена зареєстрованих bean definitions:
import java.util.Arrays;
// context уже створено (наприклад, через AnnotationConfigApplicationContext).
String[] names = context.getBeanDefinitionNames();
// Зазвичай список довгий: тут будуть і ваші біни, і інфраструктура Spring.
System.out.println(Arrays.toString(names)); // [appConfig, orderStore, scenarioRunner, ...]
Цей вивід часто виглядає довгим, тому що Spring сам додає інфраструктурні біни. Поки не лякайтеся: нас цікавить те, що ви явно додали (наприклад, orderStore, scenarioRunner), і те, що повʼязане з вашою поточною проблемою.
Ще одна корисна річ — перевірити наявність біна за імʼям. Це зручно, коли ви не впевнені, як саме він називається:
// Перевіряємо наявність біна за імʼям (зручно для діагностики та тимчасових перевірок).
boolean exists = context.containsBean("orderStore");
System.out.println(exists); // true
У навчальному проєкті це можна використовувати як діагностичний «промацувальний» код, який потім видаляється. У реальному застосунку такі перевірки краще тримати або в тестах, або в ізольованій діагностиці, а не в бізнес-логіці.
9. Типові помилки під час роботи з bean name та alias
Помилка № 1: плутати імʼя біна й імʼя класу.
Новачок бачить клас InMemoryOrderStore і намагається зробити getBean("InMemoryOrderStore"). Контейнер живе іменами бінів, а не іменами класів. У нашій конфігурації імʼя береться з @Bean-методу, тому правильніше думати «бін orderStore типу OrderStore», а не «клас InMemoryOrderStore».
Помилка № 2: використовувати getBean("name") без указання типу і потім робити каст «на удачу».
Це призводить до неприємних ClassCastException і ситуації «воно впало не там, де я очікував». У навчальному проєкті краще одразу звикнути: якщо вже йдемо за імʼям, то робимо getBean("name", SomeType.class). Це дає fail-fast і більш людські помилки.
Помилка № 3: вважати, що alias створює другий екземпляр.
Alias — це не «копія біна» і не «ще один обʼєкт». Це друга назва для того самого біна. Тому порівняння a == b при доступі за основним імʼям і за alias дає true, і це нормальна поведінка. Якщо ви очікували два різні обʼєкти, значить переплутали alias з іншим bean definition.
Помилка № 4: розкидати context.getBean(...) по всіх сервісах, бо «так простіше».
На короткій дистанції справді здається, що так простіше: «не треба думати, що передавати». Але вже через три-чотири класи ви отримуєте приховані залежності, крихкі тести та проблеми складання залежностей, які проявляються надто пізно. Spring і DI задумувалися рівно для протилежного: залежності мають бути очевидними, а не шукатися динамічно.
Помилка № 5: перейменувати @Bean-метод і здивуватися, що пошук за імʼям зламався.
Якщо ви привʼязалися до імені біна як до рядка, то перейменування методу scenarioRunner() на runner() змінить і імʼя біна за замовчуванням. У підсумку getBean("scenarioRunner", ...) перестане працювати. Якщо імʼя важливе як контракт, задавайте його явно (@Bean("scenarioRunner")) або використовуйте alias на час міграції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ