1. От DI к контейнеру
Когда вы впервые делаете DI по-честному, через конструктор, появляется приятное ощущение порядка: зависимости стали явными, код читается лучше, тесты писать проще. А потом приходит вопрос, от которого у новичков иногда начинает подёргиваться левый глаз: «Окей… а кто теперь создаёт эти объекты и передаёт их в конструктор?» В маленьком примере это делал ваш main(), но в настоящем приложении «main как сборщик всего мира» быстро превращается в огромного монстра.
Spring решает эту проблему с помощью специального объекта-контейнера, который становится «сборочным цехом» приложения. Он знает, какие объекты нужно создать, в каком порядке, какие из них зависят от других, и как выдать вам уже готовый, связанный между собой набор компонентов. Этот контейнер в Spring обычно представлен интерфейсом ApplicationContext.
Важно поймать одну мысль: DI сам по себе не «создаёт» зависимости. DI — это стиль, а контейнер — механика, которая делает этот стиль рабочим в масштабе всего приложения.
Что такое бин
Слово bean звучит так, будто Spring пытался продать нам фасоль как архитектурный паттерн. На практике всё гораздо проще: бин — это объект, жизнью которого управляет Spring. Да, технически это обычный объект в памяти JVM. Но концептуально он отличается тем, что создаётся, хранится и связывается не вашим new, а контейнером.
Представьте разницу между «я сам приготовил себе кофе на кухне» и «я пришёл в кофейню, и мне сделали кофе по правилам заведения». Кофе тот же самый — объект, — но процессы вокруг разные: кто готовит, кто отвечает за качество, где хранится информация, что делать, если чего-то не хватает. Spring-бин — это «кофе из кофейни»: вы просите — контейнер выдаёт, и он же отвечает за то, как этот объект появился.
Чтобы не путаться, полезно держать в голове такую табличку:
| Термин | Простыми словами | В нашем контексте курса |
|---|---|---|
| Java-объект | Результат new (или фабрики) | Может существовать где угодно |
| Bean (бин) | Java-объект, который создал и контролирует Spring | Используется для сервисов, репозиториев, конфигурации, инфраструктуры |
| Container | «Умное хранилище» + фабрика объектов + сборщик зависимостей | В Spring это ApplicationContext |
И ещё одна важная деталь: не каждый класс проекта является бином. Бином становится только то, что контейнер знает как создать и зачем хранить. Для своих классов это знание появляется либо через метки компонентов, либо через явную конфигурацию. Сейчас нам важно понять саму идею «управляемого объекта».
2. ApplicationContext как центр Spring
Если выбирать одно слово-якорь для понимания Spring, то это, честно, не «аннотация», а контекст. ApplicationContext — это основной интерфейс контейнера Spring, и именно он превращает набор классов в реально работающее приложение. Он хранит «каталог бинов», умеет их создавать, связывать зависимостями и отдавать по запросу.
Если упростить до рабочей картинки, ApplicationContext — это центр управления полётами. Он не делает бизнес-логику за вас — это ваша работа, — но организует пространство: кто существует, кто от кого зависит, кто когда создаётся, кто когда готов работать. Когда Boot-приложение стартует, ключевое событие — не «выполнился main()», а «поднялся ApplicationContext».
У ApplicationContext много способностей — в Spring Core он действительно «многостаночник», — но в рамках этой лекции нам важны четыре:
1) он содержит бины (управляемые объекты),
2) он строит граф зависимостей (связывает бины друг с другом),
3) он управляет жизненным циклом этих объектов на базовом уровне (создать → подготовить → сделать доступным),
4) он служит точкой доступа к миру Spring (но важно не превращать это в «глобальную переменную», мы ещё поговорим об этом).
Чтобы увидеть это не абстрактно, давайте посмотрим, где контекст появляется в Boot-приложении.
3. Откуда берётся ApplicationContext
На старте Boot-приложения есть ритуальная строчка, которую многие печатают на автомате, не особенно вникая в её смысл. Сейчас мы сделаем её чуть менее магической: SpringApplication.run(...) возвращает контекст. То есть вы буквально получаете объект, который представляет собой контейнер всего приложения.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Запускаем Spring Boot и получаем "контейнер" со всеми бинами приложения
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Просто демонстрация: какой конкретно класс контекста поднялся
System.out.println(context.getClass().getSimpleName()); // ...ApplicationContext (пример)
}
}
Внутренностей у run(...) много, но для этого места нам нужен один факт: Boot запускает не «набор аннотаций», а контейнер.
И вот очень показательный трюк. В обычной Java, если вы два раза напишете new, вы получите два разных объекта. В Spring, если вы просите один и тот же бин дважды, вы чаще всего получаете один и тот же объект, потому что контейнер хранит его у себя.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Поднимаем контекст — после этого бины можно запрашивать у контейнера
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Достаём один и тот же бин два раза (по типу)
CatalogServiceApplication a = context.getBean(CatalogServiceApplication.class);
CatalogServiceApplication b = context.getBean(CatalogServiceApplication.class);
// Для singleton-бина (по умолчанию) это будет один и тот же объект
System.out.println(a == b); // true
}
}
Здесь важно не сделать неверный вывод. CatalogServiceApplication доступен из контекста как конфигурационная точка входа самого Boot-приложения. Это не означает, что любой класс проекта автоматически становится бином: для обычных компонентов контейнер должен либо сам найти их как компоненты, либо получить явную регистрацию.
Этот пример важен не ради конкретного класса приложения, а ради ощущения: контекст — это место, где объект живёт как «единая сущность приложения», а не как случайный new где-то в коде.
4. Граф зависимостей
Слово «граф» звучит как что-то из математики, после чего у многих появляется желание тихо выйти из аудитории и сделать вид, что их тут не было. Но здесь граф — это просто удобная метафора. Граф зависимостей — это схема вида «кто от кого зависит», где узлы — это бины, а стрелки — зависимости, обычно то, что приходит в конструктор.
Возьмём тот же мини-граф catalog-service. У сервиса есть контракт репозитория, а над сервисом легко достраивается контроллер — так уже видна вся цепочка зависимостей:
import java.util.List;
interface CourseCatalogRepository {
List<String> findAllSlugs();
}
class InMemoryCourseCatalogRepository implements CourseCatalogRepository {
@Override
public List<String> findAllSlugs() {
// Репозиторий: источник данных (пока условный, без реальной БД)
return List.of("spring-boot", "spring-core");
}
}
class CourseCatalogService {
// Сервис: бизнес-логика, зависит от репозитория
private final CourseCatalogRepository repository;
// DI через конструктор: зависимость видна явно
CourseCatalogService(CourseCatalogRepository repository) {
this.repository = repository;
}
}
class CourseCatalogController {
// Контроллер: точка входа (например, HTTP), зависит от сервиса
private final CourseCatalogService service;
// DI через конструктор: зависимость видна явно
CourseCatalogController(CourseCatalogService service) {
this.service = service;
}
}
Если описать зависимости словами, получится так: controller зависит от service, service зависит от repository. Вот и весь «страшный граф». Его можно нарисовать так:
flowchart TD
Controller[CourseCatalogController] --> Service[CourseCatalogService]
Service --> Repo[CourseCatalogRepository]
Что делает Spring-контейнер? Он берёт эту картину и выполняет роль «умного сборщика»: понимает, что, чтобы создать CourseCatalogController, нужно сначала создать CourseCatalogService, а чтобы создать CourseCatalogService, нужно сначала создать CourseCatalogRepository.
Вручную это выглядело бы так же, только без контейнера:
// Ручная сборка графа зависимостей без контейнера
CourseCatalogRepository repository = new InMemoryCourseCatalogRepository();
CourseCatalogService service = new CourseCatalogService(repository);
CourseCatalogController controller = new CourseCatalogController(service);
Spring делает то же самое, но уже на уровне всего приложения и автоматически, когда вы правильно зарегистрируете бины — как именно, это следующая лекция про component scanning. И вот почему ApplicationContext так важен: он не просто «хранилище», он ещё и механизм построения и удержания этого графа.
5. ApplicationContext как Map
Новичкам часто проще начать с очень приземлённой модели: «Контекст — это как Map, где ключ — имя или тип, а значение — объект». Эта модель не полная, но полезна как стартовая ступенька, чтобы перестать бояться абстракции.
Представьте такой псевдо-код — не Spring, а просто идея:
// Псевдо-пример: не Spring-код, а интуиция
Map<String, Object> container = new HashMap<>();
// "Регистрируем" объекты в контейнере под именами
container.put("repository", new InMemoryCourseCatalogRepository());
// Вытаскиваем зависимость и передаём её в конструктор вручную
container.put(
"service",
new CourseCatalogService((CourseCatalogRepository) container.get("repository"))
);
Это уже похоже на контейнер, но у Spring поверх этой идеи есть несколько важных вещей. Он умеет сам вычислять порядок создания, умеет делать инъекцию по типу, умеет создавать объекты лениво или сразу, умеет отслеживать жизненный цикл и хранить метаданные о бинах. То есть ApplicationContext — это «умный контейнер», а не просто склад.
Но хорошая новость в том, что для базового понимания вам сейчас достаточно вот чего: в контексте живут бины, и именно оттуда Spring берёт зависимости для DI. Всё остальное будем добавлять постепенно, иначе вместо демистификации получится новый вид мистики.
6. getBean(...): инструмент и анти-паттерн
Когда вы узнаёте, что у ApplicationContext есть метод getBean(...), появляется соблазн: «О! Тогда я в любом месте возьму контекст и буду вытаскивать что захочу». И вот тут важно вовремя остановиться, потому что так рождается анти-паттерн под названием Service Locator — контейнер как «глобальный справочник», — и код снова становится неявным и плохо тестируемым.
Правильнее думать так: в прикладном коде зависимости должны приходить через конструктор, а не доставаться из контекста. Контекст — это инфраструктура, а не «библиотекарь в каждом классе».
Тем не менее getBean(...) бывает полезен, особенно на старте обучения. Это хороший инструмент для:
1) быстрой проверки, что бин реально существует,
2) демонстраций и экспериментов в main(),
3) некоторых тестовых сценариев, когда вы явно проверяете wiring.
Вот пример «нормального» использования в main() как учебного микроскопа, а не как основы архитектуры:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import java.util.Arrays;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Поднимаем контекст, чтобы увидеть, какие бины в нём зарегистрированы
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Получаем имена всех определений бинов (definition, а не обязательно уже созданные объекты)
String[] names = context.getBeanDefinitionNames();
Arrays.sort(names);
// Просто демонстрация: выводим первый бин после сортировки
System.out.println("First bean = " + names[0]); // First bean = ... (пример)
}
}
А вот пример подхода, который выглядит как «работает», но с точки зрения архитектуры — шаг назад:
import org.springframework.context.ApplicationContext;
class BadStyleService {
private final ApplicationContext context;
BadStyleService(ApplicationContext context) {
// Контекст приехал как зависимость, но дальше начинается "магия"
this.context = context;
}
void doWork() {
// Зависимость спряталась: по коду класса непонятно, что ему реально нужно
Object something = context.getBean("something");
System.out.println(something);
}
}
Здесь проблема та же, что и с new внутри класса: зависимости становятся скрытыми. Только теперь вместо new у нас «волшебный сундук» context.getBean(...). Поэтому правило на ближайшие лекции простое: контекст в прикладные классы не таскаем, а зависимости делаем явными.
7. Ошибки сборки графа зависимостей
Spring довольно добрый — по меркам фреймворков, конечно. Если он не может собрать приложение, он падает сразу и достаточно честно говорит, что именно ему не понравилось. Это как строгий преподаватель: неприятно, но полезно — лучше сейчас, чем в продакшене через два часа после релиза.
Самая частая ошибка новичка звучит так: «Я написал класс, значит Spring его знает». Нет, не знает. Если класс не зарегистрирован как бин, то контейнер не сможет его выдать и не сможет внедрить как зависимость.
В таком случае вы увидите ошибку вида «нет такого бина» — по сути NoSuchBeanDefinitionException. Например, такой код, если CourseCatalogService не стал бином, при запуске приведёт к падению:
// Пытаемся получить бин по типу.
// Если контейнер не знает про этот тип, здесь будет исключение.
CourseCatalogService service = context.getBean(CourseCatalogService.class);
// throws exception, если бин не зарегистрирован
Другая распространённая ситуация: «У меня есть тип, но подходящих бинов несколько». Тогда контейнер не угадывает «на глаз» — и правильно делает, — а сообщает о неоднозначности, по сути NoUniqueBeanDefinitionException. Пока мы не разбираем, как это решается: будут @Primary, @Qualifier и другие механики позже. Нам сейчас важна идея: контейнер не маг и не экстрасенс. Он собирает приложение по правилам и явно говорит, когда правил не хватает.
И ещё один важный психологический момент. Ошибка на старте Boot-приложения — это не «всё сломалось навсегда», а просто сигнал: «контейнер не смог построить граф». Часто это даже полезнее, чем если бы всё «как-то стартануло», а потом тихо работало неправильно.
8. Мини-диагностика контекста
Пока мы ещё не дошли до «собственных» бинов проекта, полезно уметь смотреть на контекст как на живой объект. Это снимает страх: вы видите, что контейнер — не чёрная дыра, а вполне наблюдаемая структура.
Самый простой приём — посмотреть количество бинов и убедиться, что там действительно «жизнь кипит», даже если вы пока почти ничего не написали:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class CatalogServiceApplication {
public static void main(String[] args) {
// Поднимаем приложение и получаем контекст
ApplicationContext context =
SpringApplication.run(CatalogServiceApplication.class, args);
// Сколько определений бинов знает контейнер (включая инфраструктуру Spring/Boot)
System.out.println("Beans = " + context.getBeanDefinitionCount()); // Beans = 100+ (пример)
}
}
Не пугайтесь числа. Большая часть этих бинов — инфраструктура Spring/Boot, и это нормально: фреймворк приносит с собой много готовых деталей, чтобы вам не писать их вручную. Позже, когда мы будем говорить про starters и auto-configuration, вы начнёте узнавать «кто все эти люди».
Ещё один аккуратный трюк — проверить, есть ли бин в принципе, не доставая его напрямую:
// Проверяем наличие бина по имени (имя зависит от правил нейминга Spring)
boolean exists = context.containsBean("catalogServiceApplication");
// Демонстрация: контейнер может ответить "есть/нет" без создания и выдачи самого объекта
System.out.println(exists); // true (пример для стандартного имени)
Здесь есть нюансы с именованием бинов — это отдельная тема, — поэтому воспринимайте пример как демонстрацию возможностей, а не как «жёсткий контракт». Главное сейчас — вы поняли: контекст можно «пощупать» и задавать ему вопросы.
9. Типичные ошибки при работе с ApplicationContext
Ошибка №1: считать, что любой класс проекта автоматически становится бином.
Очень легко перепутать «я написал класс» и «контейнер теперь знает, как его создать». Spring работает только с теми объектами, которые зарегистрированы как бины. Если контейнер не знает про ваш класс, он не сможет внедрить его как зависимость и не сможет выдать через getBean(...).
Ошибка №2: превращать ApplicationContext в глобальную переменную и носить его по всему коду.
Иногда кажется удобным прокинуть контекст в сервисы и доставать зависимости по месту: «ой, мне тут ещё один объект нужен». Но это убивает прозрачность зависимостей и возвращает нас к проблемам из первой лекции: код становится трудно читать и ещё труднее тестировать. В нормальном стиле зависимости видны в конструкторе.
Ошибка №3: активно использовать getBean(...) в бизнес-логике.
getBean(...) — полезный инструмент для диагностики, прототипов и редких инфраструктурных случаев. Но если ваш сервис на каждом шаге делает context.getBean(...), вы фактически пишете приложение в стиле service locator и прячете архитектуру в runtime-вызовах. В результате непонятно, что именно нужно классу для работы, и ошибки проявляются позже.
Ошибка №4: путать «контекст поднялся» и «приложение действительно готово к работе».
На этом этапе курса мы упрощаем картину, но важно понимать базовую идею: подъём ApplicationContext — это создание и связывание бинов. У реальных приложений часто есть ещё дополнительные шаги готовности — загрузка данных, проверка конфигурации, прогрев и т.д. Мы к этим темам подойдём позже, но уже сейчас полезно не думать, что «стартануло — значит всё идеально».
Ошибка №5: бояться стартовых ошибок и пытаться «заглушить» их вместо понимания.
Когда Spring падает на старте, это обычно означает одну из двух вещей: не хватает бина или есть неоднозначность, — либо какая-то другая проблема в сборке зависимостей. Самый плохой путь — пытаться «обойти» это случайными правками. Гораздо полезнее читать сообщение об ошибке как подсказку: контейнер прямо говорит, какой кусок графа зависимостей он не смог построить.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ