Сьогодні ми познайомимося з трьома китами, на яких будується Spring: IoC (інверсія керування), DI (впровадження залежностей) і Spring Beans. Усе це звучить як магія, але насправді — це найкращі друзі будь-якого Java-розробника.
1. Інверсія керування (Inversion of Control)
Згадаймо стару добру об'єктно-орієнтовану розробку. В об'єктно-орієнтованому програмуванні часто виникає ситуація, коли один об'єкт залежить від іншого. Зазвичай розробник сам створює залежний об'єкт прямо в коді:
public class Car {
private Engine engine;
public Car() {
this.engine = new Engine();
}
}
Ніби працює, але є проблема: ви «жорстко зв'язали» об'єкт Car з об'єктом Engine. Якщо потрібно підставити інший тип двигуна, наприклад, ElectricEngine, доведеться лізти в код Car і змінювати його. Не дуже зручно, та й можна випадково щось поламати! Таке змінення порушує принцип гнучкості і підтримки коду.
І ось тут на сцену виходить IoC (Inversion of Control). Ідея проста: замість того, щоб об'єкт сам створював свою залежність (Engine у нашому випадку), керування цією задачею передається контейнеру.
Контейнер IoC у Spring
Spring надає свій IoC-контейнер, який займається створенням, конфігуруванням і керуванням життєвим циклом об'єктів. Усе, що потрібно вам — це описати залежності (ми покажемо, як саме). Це дозволяє позбутися від жорстких зв'язків і зосередитися на бізнес-логіці, а не на створенні об'єктів.
Приклад:
public class Car {
private Engine engine;
// Залежність передається через конструктор
public Car(Engine engine) {
this.engine = engine;
}
}
Тепер машина Car не знає, який саме двигун Engine вона отримає. Це вирішує контейнер IoC, звільняючи нас від головного болю. У цьому коді самого контейнера IoC немає — це просто приклад класу, який готовий до використання IoC-контейнера.
2. Dependency Injection (DI): ключ до керування залежностями
Dependency Injection (DI) — це спосіб реалізації IoC. Контейнер Spring впроваджує залежності (наприклад, Engine) у потрібний об'єкт (Car), використовуючи один із наступних підходів:
- Через конструктор.
- Через сеттер (метод).
- Через поле (анотація).
На практиці DI виглядає як магія. Наприклад, так:
@Component
public class Engine {
// Клас двигуна
}
@Component
public class Car {
private Engine engine;
// Spring автоматично впровадить залежність через конструктор
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
}
Spring бачить, що клас Car залежить від Engine, і автоматично створює Engine, передаючи його в Car. Вам не потрібно самим турбуватися про створення або передачу об'єктів!
3. Spring Beans: будуємо магію під контролем
Spring Bean — це об'єкт, яким управляє IoC-контейнер. Саме контейнер створює біни, налаштовує їх, "зводить" їх залежності і керує їх життєвим циклом. Якщо вам потрібен об'єкт, просто скажіть контейнеру: «Ей, дай мені бин!», і він виконає вашу прохання.
Клас можна перетворити на бін, якщо позначити його спеціальними анотаціями:
@Component— універсальна анотація для створення бінів.@Service— позначає біни, які виконують бізнес-логіку.@Repository— для бінів, які працюють з даними (наприклад, репозиторії JPA).@Controller— для бінів, які обробляють HTTP-запити (в контексті Spring MVC).
Приклад створення біна
@Component
public class Engine {
// Це бін Spring. Тепер контейнер буде управляти цим об'єктом.
}
@Component
public class Car {
private final Engine engine;
@Autowired // Кажемо Spring автоматично впровадити залежність
public Car(Engine engine) {
this.engine = engine;
}
}
4. Способи впровадження залежностей
Тепер давайте розберемо три основні способи впровадження залежностей за допомогою DI.
- Конструкторне впровадження
Цей спосіб переважний, бо робить залежності очевидними і immutable. Ось приклад:
@Component
public class Car {
private final Engine engine;
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
}
- Сеттерне впровадження
Плюс такого підходу — гнучкість. Залежність можна замінити після створення об'єкта. А мінус в тому, що це може привести до зміни стану об'єкта, що не завжди добре.
@Component
public class Car {
private Engine engine;
@Autowired
public void setEngine(Engine engine) {
this.engine = engine;
}
}
- Впровадження через поле (анотації)
Найбільш спірний спосіб, оскільки порушує принцип інкапсуляції. Але для простих випадків — цілком життєздатний варіант.
@Component
public class Car {
@Autowired
private Engine engine;
}
5. Життєвий цикл Spring бінів
- Створення — контейнер створює об'єкт.
- Ініціалізація — контейнер передає залежності і викликає методи, такі як
@PostConstruct. - Використання — бін готовий працювати.
- Завершення — контейнер завершує роботу біна і викликає методи, такі як
@PreDestroy.
Методи для керування життєвим циклом:
@PostConstruct— метод викликається після створення біна і завершення DI.@PreDestroy— метод викликається перед знищенням біна.
Приклад коду:
@Component
public class Car {
@PostConstruct
public void init() {
System.out.println("Автомобіль готовий до руху!");
}
@PreDestroy
public void destroy() {
System.out.println("Автомобіль знищується...");
}
}
6. Поширені помилки при роботі з IoC і DI
Не переймайтеся, помилки трапляються в усіх. Ось класичний випадок: ви забули позначити клас Engine анотацією @Component. Через це Spring не може знайти бін:
Error creating bean with name 'car': Unsatisfied dependency expressed through constructor
Підказка: перевірте, чи всі залежності "включені" в контекст, тобто мають анотації типу @Component.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ