Сегодня мы познакомимся с тремя китами, на которых строится 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 Bean'ов
- Создание — контейнер создаёт объект.
- Инициализация — контейнер передаёт зависимости и вызывает методы, такие как
@PostConstruct. - Использование — бин готов работать.
- Завершение — контейнер завершает работу бина и вызывает методы, такие как
@PreDestroy.
Методы для управления жизненным циклом:
@PostConstruct— метод вызывается после создания бина и завершения DI.@PreDestroy— метод вызывается перед уничтожением бина.
Пример кода:
@Component
public class Car {
@PostConstruct
public void init() {
System.out.println("Car is ready to drive!");
}
@PreDestroy
public void destroy() {
System.out.println("Car is being destroyed...");
}
}
6. Частые ошибки при работе с IoC и DI
Не беспокойтесь, ошибки бывают у всех. Вот классический случай: вы забыли пометить класс Engine аннотацией @Component. Из-за этого Spring не может найти бин:
Error creating bean with name 'car': Unsatisfied dependency expressed through constructor
Подсказка: проверьте, все ли зависимости "включены" в контекст, то есть имеют аннотации типа @Component.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ