Коли програміст-початківець вперше відкриває документацію до Spring Framework, на нього одразу висипається купа грізних термінів: Dependency Injection (DI), Inversion of Control (IoC), ApplicationContext, Bean. Здається, що це вища математика. Ми вже розглядали DI в минулих матеріалах але цю дуже не просту тему краще розібрати по болтиках для повного розуміння.
Насправді за цими розумними словами ховається надзвичайно проста та красива ідея. Це історія про те, як перестати збирати складні речі вручну й довірити цю роботу «розумному конвеєру». Давайте розберемося, що таке Dependency Injection (впровадження залежностей), навіщо воно потрібне і як Spring робить це під капотом — без складної академічної термінології, а буквально на пальцях.
Що таке «залежність» у коді?
Уявімо, що ви вирішили побудувати реальний автомобіль. У вашому Java-коді це буде клас Car. Але автомобіль не може їхати сам по собі — йому потрібен двигун (Engine).
Коли один клас використовує функції іншого класу для своєї роботи, у програмуванні це називають залежністю (dependency). У нашому випадку клас Car залежить від класу Engine.
Як би ми реалізували це без Spring, використовуючи класичний підхід?
public class Engine {
public void start() {
System.out.println("Врум-врум! Двигун працює.");
}
}
public class Car {
private Engine engine;
public Car() {
// Автомобіль сам створює собі двигун!
this.engine = new Engine();
}
public void drive() {
engine.start();
System.out.println("Машина поїхала.");
}
}JavaЗдається, усе працює чудово. Але що тут не так?
- Жорстка прив’язка (Tight Coupling): Наш автомобіль сам створює двигун за допомогою ключового слова
new. Він намертво «припаяний» до конкретного типу двигуна. Якщо завтра ми захочемо створити електромобіль (ElectricEngine), нам доведеться переписувати весь класCar. - Проблеми з тестуванням: Ви не зможете протестувати клас
Carокремо відEngine. Якщо в реальному двигуні є помилка (наприклад, він намагається підключитися до бази даних або зовнішнього API), ваш тест автомобіля впаде, хоча сам автомобіль спроектований ідеально. - Порушення Single Responsibility (Єдиної відповідальності): Автомобіль має відповідати за рух. Натомість він змушений виконувати роль автозаводу — розбиратися, як саме створювати двигун, які параметри йому передавати в конструктор тощо.
Що таке Dependency Injection (DI)?
Dependency Injection (Впровадження залежностей) — це підхід, при якому клас не створює свої залежності сам. Замість цього він отримує їх ззовні у готовому вигляді.
Уявіть, що ви купуєте стіл в IKEA. Ви не йдете до лісу рубати дерево та виточувати ніжки (new Legs()). Ви просто отримуєте коробку з готовими деталями та інструкцією, як їх з’єднати. Ніжки вам «впроваджує» фабрика IKEA.
Давайте перепишемо наш приклад без жорсткої залежності:
public class Car {
private Engine engine;
// Двигун передається ззовні через конструктор!
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Машина поїхала.");
}
}JavaТепер клас Car став «чистим». Йому байдуже, як саме було створено двигун Engine. Він просто каже: «Дайте мені будь-який двигун, і я поїду!». Ми можемо передати туди бензиновий двигун, дизельний чи електричний. Ми повністю розірвали жорсткий зв’язок.
Проте виникає логічне запитання: якщо сам Car більше не створює двигун, то хто це має робити? Хтось же повинен написати new Engine() і передати його в new Car(engine)?
Ось тут на сцену виходить Spring у всій своїй красі. Починається найцікавіше.
Inversion of Control та чарівний контейнер Spring
У традиційній програмі ви, як розробник, повністю контролюєте життєвий цикл об’єктів: самі пишете new, самі викликаєте методи.
У Spring усе працює навпаки. Цей принцип називається Inversion of Control (IoC) — Інверсія керування. Ви більше не керуєте створенням об’єктів. Ви віддаєте цей контроль Spring, а він стає вашим особистим “автозаводом” та “складальним цехом”.
Цей складальний цех у Spring називається IoC-контейнером (або ApplicationContext).
Об’єкти, які створюються, налаштовуються та керуються цим контейнером, у Spring називаються Бінами (Beans). Це просто звичайні Java-об’єкти, але “під наглядом” фреймворку.
Як це виглядає у Spring-додатку?
Для того, щоб Spring зрозумів, які класи потрібно перетворити на “біни” та як їх між собою з’єднати, ми використовуємо спеціальні інструкції — анотації.
Давайте подивимося на сучасний варіант з використанням Spring Boot:
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
// 1. Кажемо Spring: "Ей, візьми цей клас під свій контроль і створи його об'єкт!"
@Component
public class Engine {
public void start() {
System.out.println("Врум! Двигун запущено контейнером Spring.");
}
}
@Component
public class Car {
private final Engine engine;
// 2. Анотація @Autowired каже Spring:
// "Знайди у своєму контейнері бін типу Engine і встав його сюди!"
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Машина їде за допомогою DI!");
}
}JavaЩо відбувається, коли ми запускаємо наш Spring Boot додаток?
- Spring сканує весь проект у пошуках класів з анотацією
@Component(та її аналогами, такими як@Service,@Repository,@Controller). - Він бачить клас
Engineі створює його єдиний екземпляр у своїй пам’яті (бінengine). - Потім він бачить клас
Carі розуміє, що для його створення потрібенEngine(оскільки ми вказали це в конструкторі та поставили@Autowired). - Spring бере вже створений раніше бін
engine, вставляє його в конструкторCarта створює бінcar. - Все! Об’єкти створені та налаштовані автоматично. Вам більше ніколи не доведеться писати ключове слово
newдля бізнес-логіки вашого додатка.
Які є способи впровадження залежностей?
У Spring є три основні шляхи впровадження залежностей. Давайте коротко їх порівняємо:
- Через конструктор (Constructor Injection) — Рекомендований спосіб:
@Autowired
public Car(Engine engine) {
this.engine = engine;
}Java- Чому це найкраще? Залежність можна зробити незмінною (
final). Окрім цього, додаток не запуститься, якщо залежності немає, що вберігає від помилок типуNullPointerException. Також такий код найлегше тестувати. - Через сетер (Setter Injection):
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}Java- Коли використовувати? Коли залежність є необов’язковою і її можна змінити або додати пізніше, під час роботи програми.
- Напряму через поле (Field Injection):
@Autowired
private Engine engine; // Ні конструктора, ні сетера не потрібноJava- Чому цього слід уникати? Це виглядає дуже просто і лаконічно, але це вважається поганим тоном (bad practice). Такий код дуже важко тестувати без запуску всього важкого контейнера Spring, і він приховує архітектурні проблеми (клас може непомітно розростися до десятків залежностей).
Чому DI — це круто?
Коли ви використовуєте Dependency Injection у Spring, ви отримуєте купу переваг:
- Гнучкість: Потрібно замінити локальне сховище файлів на хмару AWS? Просто створіть новий бін, який реалізує той самий інтерфейс, і Spring автоматично “підставить” його в усі потрібні місця.
- Легке тестування: Ви можете легко підсунути вашому класу “іграшкову” залежність (Mock-об’єкт) замість реальної бази даних і перевірити лише логіку самого класу.
- Чистота коду: Код позбувається хаосу створення об’єктів. Ви фокусуєтеся виключно на написанні бізнес-логіки, а рутинну роботу зі збирання конструктора бере на себе Spring.
Тепер, коли ви почуєте фразу “Spring — це просто IoC-контейнер, який займається Dependency Injection”, ви знатимете, що це просто дуже розумний і автоматизований завод з виробництва та збирання ваших Java-класів!





