В этой статье рассказывается о шаблонах проектирования, на которых строится работа со Spring-контейнером и о том, каким классом он представлен во фреймворке Spring.
Мы создадим простейший пример, в котором конфигурируются бины, создается контейнер и из него извлекаются готовые бины.
Инверсия управления (Inversion of Control) – что это
Ключевая особенность приложения, написанного на Spring, состоит в том что большую часть объектов создаем не мы, а Spring. Мы лишь конфигурируем классы (с помощью аннотаций либо в конфигурационном XML), чтобы «объяснить» фреймворку Spring, какие именно объекты он должен создать за нас, и полями каких объектов их сделать. Spring управляет созданием объектов и потому его контейнер называется IoC-контейнер. IoC расшифровывается как Inversion of Control. А объекты, которые создаются контейнером и находятся под его управлением, называются бинами.
Иллюстрировать это можно так:
В общем на вход контейнер Spring принимает:
- Наши обычные классы (которые впоследствии будут бинами).
- Конфигурацию (неважно как именно ее задавать – либо в специальном файле XML, либо с помощью специальных аннотаций).
А на выходе он производит объекты – бины. То есть экземпляры классов, созданные в соответствии с конфигурацией и внедренные куда нужно (в другие бины). После этого никакие операторы new нам не понадобятся, мы будем работать в классе-бине с его полями-бинами так, будто они уже инициированы. Конечно, не со всеми полями, а только с теми, которые сконфигурированы как бины. Остальные инициализируются как обычно, в том числе с помощью оператора new.
Что такое внедрение зависимости (Dependency Injection)
Внедрение зависимости — это и есть инициализация полей бинов другими бинами (зависимостями).
Ведь помимо создания объектов, Spring-контейнер внедряет эти объекты в другие объекты, то есть делает их полями других объектов. Иногда это выглядит магически – например, контейнер способен внедрить зависимость в поле с модификатором private, для которого нет сеттера. Как же код Spring может это сделать? Дело в том, что под капотом он использует рефлексию, так что это реально. Но эти детали для нас как разработчиков не важны, главное знать, как объяснить фреймворку, какие объекты вы хотите отдать под его управление, и в какие поля других объектов вы хотите их внедрить.
Кстати, шаблон Dependency Injection не привязан к Spring, это всего лишь инициализация поля класса. Это такая обычная вещь и так часто встречается в коде (через конструктор либо сеттер), что даже странно выделять ее в отдельный шаблон. В связи со Spring это название мелькает часто наверно потому, что внедрение выполняет Spring, и у программиста тут много возни с конфигурацией зависимостей. Но с другой стороны, создание и внедрение Spring-ом — это уже другой шаблон — инверсия контроля (IoC).
Класс ApplicationContext для работы с IoC Контейнером
Конфигурация Maven
Чтобы иметь возможность работать с контейнером, добавьте в pom.xml зависимость:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.3.RELEASE</version> </dependency>
Последнюю версию зависимости можно взять тут.
Класс ApplicationContext
Для работы с контейнером существует не один класс. Но удобнее всего работать с классом ApplicationContext. Чтобы инициализировать контейнер и создать в нем бины, нужно создать экземпляр класса ApplicationContext.
Как уже сказано, контейнеру для создания бинов. требуется конгфигурация, так что конструктор контейнера принимает аргумент. Существуют два подкласса ApplicationContext: ClassPathXmlApplicationContext берет конфигурацию из XML-файла, а AnnotationConfigApplicationContext – из аннотаций:
ApplicationContext xmlConfigContext = new ClassPathXmlApplicationContext("config.xml"); ApplicationContext javaConfigContext = new AnnotationConfigApplicationContext(Config.class);
Так что контейнер можно создать, используя именно тот вид конфигурации, которая используется в вашем приложении. Обычно это аннотации, XML немного устарел.
В конфигурации прописаны как исходные классы, которые надо сделать бинами, так и их зависимости, а также каким образом внедрить эти зависимости. Обычно зависимость внедряется либо через конструктор, либо через сеттер, в зависимости от дизайна класса.
После того, как мы инициализировали контейнер, и он создал бины, появляется возможность их получить непосредственно из контейнера. Хотя это не всегда нужно, но можно. Делается это так:
Animal animal = javaConfigContext.getBean(Animal.class);
Мы получили бин типа Animal из контейнера в предположении, что он сконфигурирован в нашем приложении. Давайте теперь его на самом деле сконфигурируем.
Конфигурация бинов с помощью аннотаций
Задавать конфигурацию будем с помощью аннотаций, поскольку это более современный и удобный способ.
Во-первых, создадим класс конфигурации и аннотируем его с помощью @Configuration:
@Configuration public class Config { }
Во-вторых, допустим, у нас есть класс Animal, который мы хотим делать бином:
public class Animal { }
Чтобы сделать Animal бином, создадим в классе конфигурации метод,который создает и возвращает Animal, и аннотируем этот метод с помощью @Bean:
@Configuration public class Config { @Bean public Animal createAnimal() { return new Animal(); } }
Все, бин сконфигурирован. Теперь экземпляр Animal будет создаваться контейнером автоматически, и мы сможем получить его из контейнера, как показано выше.
Возможно вы спросите, в чем смысл этого, ведь все равно мы создаем экземпляр Animal отдельным методом. Где же польза контейнера?
Да, мы прописываем метод, создающий Animal. Но мы не вызываем этот метод. Мы просто создаем контейнер. А метод вызывается контейнером во время его инициализации. И учитывая то, что бины обычно конфигурируются однотипно, выгода есть. К тому же они обычно создаются контейнером в единственном экземпляре (хотя это зависит от конфигурации) и внедряются в поля других бинов согласно конфигурации автоматически.
Причем вместо создания аннотированного @Bean метода, можно аннотировать класс Animal изнутри аннотацией @Component – а это и вовсе одна строчка.
О том, как сконфигурировать внедрение бинов в поля других бинов, читайте статью про внедрение зависимостей.
Разница между аннотациями @Bean и @Component в том, что @Bean более гибкая аннотация, ею мы аннотируем метод, а не класс:
- С помощью @Bean можно конфигурировать бины для тех классов, код которых вы не можете редактировать, например, классы из чужих библиотек.
- С помощью @Bean можно также конфигурировать классы, создаваемые фабричными методами.
Давайте сконфигурируем второй бин Man с помощью аннотации @Component:
@Component public class Man { }
Только учтите, что если мы аннотируем классы с помощью @Component, то в файл конфигурации надо добавить аннотацию @ComponentScan для автоматического поиска этих аннотированных классов. В ней надо указать имя пакета или нескольких пакетов, в который лежат эти классы. В этом пакете контейнер будет пытаться их найти автоматически.
Давайте добавим в файл конфигурации одну строчку с аннотацией @ComponentScan и именем пакета для поиска бинов:
@ComponentScan("ru.javalang.ioc") @Configuration public class Config { @Bean public Animal createAnimal() { return new Animal(); } }
Все бины, аннотированные с помощью @ComponentScan, должны лежать в пакете «ru.javalang.ioc».
Проверим, что оба бина действительно создаются:
@Test public void givenAnnotationConfig_whenContextCreated_ThenBeansGot() { ApplicationContext javaConfigContext = new AnnotationConfigApplicationContext(Config.class); Animal animal = javaConfigContext.getBean(Animal.class); Man man = javaConfigContext.getBean(Man.class); assertNotNull(animal); assertNotNull(man); }
Итог
Мы рассмотрели, что такое IoC-контейнер, как его создать и как из него получить бины. Также мы узнали, что можно сконфигурировать бины с помощью аннотаций @Bean и @Component.
Исходый код примера для этой статьи доступен на GitHub.
Спасибо, отличное объяснение!
Все очень доступно изложили, спасибо
Вы очень хороший преподаватель, так просто объяснять сложные вещи это надо уметь. Огромное спасибо за ваши статьи!
Описано максимально понятно, при этом используются определения из практики, что облегчает понимание последующих статей. Огромное Вам спасибо, всё очень понятно и просто изложено.
Это прекрасно! Написано не заклятиями, а понятным языком.
Лучшие объяснения что я видел.