Что такое inversion of control и как spring реализует этот принцип spring
Основы Spring Framework: что такое инверсия контроля?
Разработчики, начиная с Spring Framework, часто путают терминологию, в частности, зависимости, внедрение зависимостей и инверсию управления. В этой статье мы познакомим вас с концепцией инверсии управления.
Что вы узнаете
Что такое инверсия контроля?
Подход-1
Посмотрите на следующую реализацию ComplexAlgorithmImpl :
Одна из многочисленных вещей, которая ComplexAlgorithmImpl делает сортировку. Он создает экземпляр BubbleSortAlgorithm непосредственно в своем коде.
Подход-2
Теперь посмотрите на эту реализацию для изменения:
Сравнение подхода-1 и подхода-2
Подход-1
Подход-2
Инверсия контроля при игре!
В подходе-1 ComplexAlgorithmImpl привязан к определенному алгоритму сортировки.
В «Подходе-2» говорится: дайте мне любой алгоритм сортировки, и я буду работать с ним.
Вместо того, чтобы создавать свои собственные зависимости, класс объявляет свои зависимости. Элемент управления теперь переходит от класса к пользователю класса, чтобы обеспечить зависимость.
Почему инверсия контроля важна?
После того как вы напишите код с помощью Inversion of Control, вы сможете использовать такие среды, как Spring, для завершения внедрения зависимостей и подключения bean-компонентов и зависимостей.
Преимущества инверсии контроля
Наконец, не забудьте проверить видео ниже на IoC:
Резюме
В этой статье мы говорили об инверсии управления. Вместо того чтобы класс создает экземпляр собственной зависимости, он оставляет его пользователю класса для передачи и делает код слабо связанным.
Надеюсь, вы чему-то научились! Дайте нам знать, что вы думаете в комментариях ниже.
Концепция Inversion of Control и основы Spring
Паттерны уменьшения зависимостей между компонентами системы
J2EE – большая и сложная спецификация, охватывающая, тем не менее, далеко не все нюансы реализации. Кроме того, многие реализации серверов приложений содержат возможности, выходящие за рамки спецификации. Разработка под конкретный сервер приложений вольно или невольно приводит к тому, что код приложения включает участки, зависимые от этого сервера. Это создает немало проблем при попытке переноса приложения под другой сервер. Spring переносим между разными серверами приложений, и поддерживает WebLogic, Tomcat, Resin, JBoss, WebSphere и другие серверы приложений.
Концепция, лежащая в основе инверсии управления, часто выражается «голливудским принципом»: «Не звоните мне, я вам сам позвоню». IoC переносит ответственность за выполнение действий с кода приложения на фреймворк. В отношении конфигурирования это означает, что если в традиционных контейнерных архитектурах наподобие EJB, компонент может вызвать контейнер и спросить: «где объект Х, нужный мне для работы?», то в IoC сам контейнер выясняет, что компоненту нужен объект Х, и предоставляет его компоненту во время исполнения. Контейнер делает это, основываясь на подписях методов (таких, как свойства JavaBean) и, возможно, конфигурационных данных в формате XML.
В этой статье будут рассмотрены основные паттерны ослабления связей между компонентами системы, а также использование паттерна IoC в Sping Framework.
Для рассмотрения паттернов вынесения зависимостей будем использовать простой фрагмент кода, где класс LoginManager использует класс UserList, чтобы получить доступ к списку активных пользователей системы.
Единственное тонкое место в этом коде – это то, что LoginManager зависит от UserList. У такой зависимости можно отметить следующие недостатки:
Таким образом, из-за зависимости компонентов страдает переносимость и возможность их повторного использования.
Рассмотрим способы выхода из этой неприятной ситуации. Необходимо сделать так, чтобы класс LoginManager мог работать с любым хранилищем пользовательской базы. Для этого определим такие хранилища, как UserStorage.
public interface UserStorage <
User getUserByName ( String theUserName ) ;
>
public class UserList implements UserStorage
public class LoginManager <
private UserStorage myUserList = new UserList () ;
.
public boolean authenticateUser ( String theUserName, String thePassword ) <
User aUser = myUserList.getUserByName ( theUserName ) ;
return thePassword.equals ( aUser.getPassword ()) ;
>
.
>
Такая организация обладает гораздо большей гибкостью. Чтобы класс был действительно универсальным, достаточно добавить setUserStorage(UserStorage theStorage) в LoginManager. Любые способы хранения пользователей могут быть легко добавлены. Этот способ открывает дорогу для создания тестов отдельно для классов UserList и LoginManager. В случае с LoginManager может быть использован класс-заглушка MockUserStorage.
Рисунок 1. Начальная диаграмма классов.
Рисунок 2. Диаграмма классов с вынесением зависимости.
Итак, мы имеем прекрасные переносимые компоненты LoginManager, UserList, JdbcUserStorage, LdapUserStorage. Не стоит думать, что мы избавились от необходимости соединять их вместе. Для использования этих компонентов необходим некий класс RuntimeAssembler, который будет делать грязную работу по соединению компонентов в единую систему.
RuntimeAssembler-классы не предназначены для повторного использования или наследования от них. В больших системах эти классы максимально запутаны. IoC-контейнеры как раз и предназначены для упрощения соединения компонентов. Все они позволяют использовать API и создавать RuntimeAssember-классы, при этом XML-конфигурация системы и RuntimeAssembler не понадобятся.
Альтернативой паттерну вынесения зависимости (Dependency Injection) является паттерн Service Locator. Он широко используется в J2EE. Так, ServiceLocator может инкапсулировать все JNDI-вызовы J2EE-системы или создание (получение) UserStorage-реализации в нашем примере. Основным отличием Dependency Injection и Service Locator является то, что в Service Locator для получения реализации UserStorage используется вызов объекта ServiceLocator (см. код ниже), в то время как в Dependency Injection ассемблер создает связь автоматически.
Рисунок 3. Диаграмма для Dependency Injection.
Рисунок 4. Диаграмма для Service Locator.
Очевидно, что в паттерне ServiceLocator есть зависимость между LoginManager и ServiceLocator, в то время как LoginManager не зависит от RuntimeAssembler в Dependency Injection. Каждый может выбирать один из этих IoC-паттернов, в зависимости от своих нужд и задач. Далее будет рассмотрен IoC-контейнер, реализованный в Spring Framework.
Введение в Spring
SpringFramework Spring Framework представляет собой набор готовых решений для использования всех основных Enterpise Java технологий — JDBC, ORM, JTA, Servlets/JSP, JMX и многих других. Абстрактные классы, фабрики и бины разработаны таким образом, чтобы программисту оставалось написать только свою логику.
Spring также содержит прекрасные классы, реализующие паттерн MVC (модель-вид-контроллер) для Web-приложений. Абстрактные DAO-классы упрощают работу с persistance layer, будь то JDBC, JDO или Hibernate, и позволяют избежать больших трудностей при смене технологии. Транзакции реализуются собственным Transaction API, который позволяет использовать различные менеджеры транзакций, в том числе JTA J2EE-сервера приложений и Hibernate. Не стоит думать, что Spring является просто IoC-контейнером, он также предоставляет фундаментальную библиотеку для использования известных Java-технологий
Рисунок 5. Возможности IoC-контейнера Spring
Простой пример
Рассмотрим особенности IoC-контейнера Spring. Сам контейнер представляет собой реализацию интерфейса BeanFactory. Обычно используется реализация XmlBeanFactory, которая читает runtime-конфигурацию из XML-файла.
Этот простой фрагмент XML создает в контейнере bean с именем helloWorld, и устанавливает его свойство message в Sergei. Реализация может быть следующей:
interface HelloWorld <
void sayMessage () ;
>
public class HelloWorldImpl implements HelloWorld <
private String myMessage;
public void setMessage ( String theMessage ) <
myMessage = theMessage;
>
public void sayMessage () <
System.out.println ( «Hello world!Hello » + myMessage + «!» ) ;
>
>
public class Application <
public static void main ( String [] args ) <
BeanFactory aBeanFactory = new XmlBeanFactory ( «sample-beans.xml» ) ;
HelloWorld aHelloWorld = ( HelloWorld ) aBeanFactory
.getBean ( «helloWorld» ) ;
// выводит «Hello world!Hello Sergei!» в System.out
aHelloWorld.sayMessage () ;
>
>
Создание объектов
В приведенном примере за конструирование объекта helloWorld отвечает контейнер – атрибут class элемента bean соответствует вызову конструктора без параметров. Spring поддерживает широкий спектр механизмов создания объектов – вызов конструктора с параметрами или без, использование фабрик классов или фабричных методов. Использование всех этих методов довольно прямолинейно, единственным нюансом является то, что при использовании конструктора с параметрами необходимо указывать либо тип, либо индекс параметра конструктора (атрибуты type и index соответственно).
При использовании статического фабричного метода не гарантируется, что созданный объект будет того же класса, где определен фабричный метод.
В элементе constructor-arg поддерживаются те же значения, что и в элементе property, полный их синтаксис будет рассмотрен далее.
В описаниях bean’ов могут быть заданы два режима работы – режим прототипа (используется по умолчанию) и режим синглтона (единственный экземпляр). В первом режиме каждый запрос объекта с заданным именем будет возвращать новый объект с указанными свойствами и связями, во втором режиме будет создан единственный экземпляр, и он будет возвращаться при каждом вызове к BeanFactory, независимо от контекста. Это поведение регулируется атрибутом singleton элемента bean.
Значение singleton по умолчанию – true.
Установка зависимостей и свойств компонентов
Жизненный цикл объектов
Контейнер Spring поддерживает методы инициализации и разрушения объектов. В любом объекте, реализующем InitializingBean, после задания значений всех декларированных свойств будет автоматически вызван метод afterPropertiesSet. На самом деле реализация этого интерфейса не обязательна, Spring предоставляет возможность вызова любого метода, указанного в атрибуте init-method в определении bean-а. Аналогичная ситуация и с методом разрушения, который вызывается при разрушении контейнера, интерфейс и атрибут называются DisposableBean и destroy-method соответственно.
Другие возможности
В этой статье кратко описаны основные возможности контейнера Spring, за более подробным описанием возможностей можно обратиться к Spring Reference Documentation, замечу лишь, что Spring активно использует не только объектно-ориентированный подход, но и аспектно-ориентированный. Аспекты и замещения методов могут быть также легко прописаны в конфигурационном файле, как и bean. Таким образом, контейнер Spring предоставляет широкий спектр опций соединения компонентов в систему, и позволяет разработчику сосредоточиться на разработке переносимых компонентов с использованием ООП и развивающейся АОП. Огромным преимуществом этого контейнера перед другими является набор классов, позволяющих использовать современные Java/J2EE технологии совместно с классами вашей предметной области.
Инверсии зависимостей управления впрыском
Вступление
Наверняка первый вопрос, который возник у вас при взгляде на заголовок, был «Шта?«. На самом деле я просто перевел фразу «Инверсия управления, внедрение зависимости» в Google Translate на китайский, а затем обратно. Зачем? Затем, что на мой взгляд, это хорошая иллюстрация того, что происходит на самом деле. Люди вокруг путают, коверкают и извращают эти понятия. По долгу службы я провожу много интервью, и 90% того, что я слышу, когда задаю вопрос про DI — честно говоря, откровенный бред. Я сделал поиск по Хабру и нашел несколько статей, которые пытаются раскрыть эту тему, но не могу сказать, что они мне сильно понравились (ладно, ладно, я проглядел только три первых страницы, каюсь). Здесь же на Хабре я встречал в комментариях такую расшифровку IoC, как Injection of Container. Кто-то всерьез предполагает, что есть некий механизм инъекции контейнеров, который сосуществует где-то рядом с DI, и, видимо, даже делает нечто похожее. Только с контейнерами. Мда. На самом деле понять внедрение зависимости очень просто, надо всего лишь…
Удивительно, но факт — эта штука со «всего лишь. » действительно работает! Иначе вы бы здесь не оказались, не так ли?
Ричард Фейнман был удивительным рассказчиком, умевшим ясно и доступно объяснять весьма сложные вещи (посмотрите, хотя бы, это видео). Джоэл Спольски считает, что по-настоящему умный программист обязательно должен уметь изъясняться на человеческом языке (а не только на Си). Ну и, наверное, практически каждому известен афоризм Альберта Эйнштейна: «Если вы что-то не можете объяснить шестилетнему ребёнку, вы сами этого не понимаете«. Конечно же, я не собираюсь сравнивать вас с шестилетними детьми, но тем не менее постараюсь рассказать про DI, IoC и еще один DI максимально просто и понятно.
NB. А вы знали, что помимо внедрения зависимости через конструктор и сеттер, существует еще и третий способ — внедрение посредством интерфейса? Хоть это и выходит за рамки данной статьи, но, держу пари, что либо вы сейчас открыли для себя кое-что новенькое, либо по крайней мере опустили уже заготовленный тухлый помидор.
Инверсия управления (Inversion of Control)
Что вы делаете в свой выходной? Может быть, читаете книги. Может быть, играете в видеоигры. Может быть, пишете код, а может потягиваете пиво за просмотром очередного сериала (вместо того, чтобы засаживать Марс яблонями). Но что бы вы ни делали, весь день в вашем полном распоряжении и только вы управляете его распорядком.
Однако, к сожалению, выходные заканчиваются, наступает понедельник, и приходится идти на работу (если, конечно, она у вас есть). По условиям трудового договора вы должны быть на месте в 8 утра. Вы работаете до полудня. Потом у вас перерыв на обед, а затем еще четыре часа кипучей деятельности. Наконец, в 17:00 вы выбираетесь из офиса и отправляетесь домой, где снова можете расслабиться и вмонтировать пивандрия. Чувствуете разницу? Вы больше не управляете своим дневным расписанием, это делает кое-кто другой — ваш работодатель.
Рассмотрим еще один пример. Допустим, вы пишете приложение с текстовым интерфейсом. В своей функции Main вы запрашиваете пользовательский ввод, ожидаете от пользователя последовательности символов, вызываете подпрограммы для обработки полученных данных (может быть, даже в отдельных потоках), а функции общего характера запрашиваете у подключенных библиотек. Таким образом вся власть сконцентрирована в ваших руках, и написанный вами код полностью управляет потоком выполнения приложения.
Но в один прекрасный день в кабинет входит босс и сообщает пренеприятнейшее известие — консоль больше не в моде, миром правят графические интерфейсы, а значит все надо переделать. Будучи современным и гибким (речь не только о ваших занятиях йогой) программистом, вы сразу принимаетесь за внесение изменений. Для этого вы подключаете GUI-фреймворк и пишете код обработки событий. Если нажата вот эта кнопка, то надо сделать то-то и то-то. А если пользователь изменил свой выбор в выпадающем списке, то не обойтись без вот этого и этого. Все идет хорошо, но тут вы понимаете, что раньше было как-то по-другому. А кто, собственно, вызывает эти обработчики событий, которые вы так усердно программируете? Кто вообще определяет, куда и когда нажал пользователь? Что вообще происходит? Где мои носки? GUI-фреймворк оказался явно хитрее, чем вы думали, и перехватил у вас управление потоком выполнения приложения.
Это и есть Inversion of Control — очень абстрактный принцип, постулирующий факт задания потока выполнения некой внешней по отношению к вам сущностью.
Понятие IoC тесно связано с понятием фреймворка. Это главная характеристика, отличающая его от другого способа оформления переиспользуемого кода — библиотеки, функции которой вы просто вызываете из своей программы. Фреймворк же — это внешний каркас, предоставляющий заранее определенные точки расширения. В эти точки расширения вы и вставляете свой код, но когда он будет вызван определяет именно фреймворк.
В качестве домашнего задания поразмышляйте, почему Джефф Сазерленд настаивает на том, что SCRUM — это именно фреймворк, а не методология.
Инверсия зависимости (Dependency Inversion)
Это та самая буква D в аббревиатуре SOLID — принцип, говорящий о том, что:
Проблема здесь заключается в том, что класс Foo зависит от конкретного класса Bar. По той или иной причине — в целях расширяемости, переиспользуемости или тестируемости ради — может встать задача их разделить. Согласно принципу инверсии зависимости для этого следует ввести между ними промежуточную абстракцию.
Диаграмма UML наглядно демонстрирует оба варианта.
Сложности начинаются, когда спрашиваешь, а где же здесь, собственно, инверсия? Основополагающая идея, без понимания которой невозможно ответить на этот вопрос, заключается в том, что интерфейсы принадлежат не своим реализациям, а использующим их клиентам. Имя интерфейса IBar вводит в заблуждение и заставляет рассматривать связку IBar + Bar как единое целое. В то же время истинным владельцем IBar является класс Foo, и если принять это во внимание, то направление связи между Foo и Bar действительно обратится вспять.
Внедрение зависимости (Dependency Injection)
Взглянув на получившийся код, внимательный читатель, конечно же, заметит, что даже несмотря на введение промежуточной абстракции, за инстантинацию класса Bar по-прежнему отвечает класс Foo. Очевидно, что это не совсем то разделение, на которое можно было рассчитывать.
Чтобы избавить класс Foo от такой неприятной обязанности, хорошо было бы вынести код инстантинации куда-то в другое место и инкапсулировать его там (поскольку все мы чрезвычайно прагматичны и не любим ничего писать дважды). Сделать это можно двумя способами — используя либо Service Locator, либо Dependency Injection.
Service Locator — это такой реестр соответствия абстракций и их реализаций. Вы скармливаете ему интересующий вас интерфейс, а в ответ получаете готовый экземпляр конкретного класса. Выглядит это примерно так:
Нюанс заключается в том, что класс Foo теперь совершенно не зависит от класса Bar, но по-прежнему управляет его инстантинацией. Как мы уже знаем, избежать этого можно инвертировав поток управления, т.е. передав оное управление в руки некоего внешнего механизма. Dependency Injection и является таким механизмом, реализуемым в виде фреймворков под названием IoC-контейнеры:
Заключение
На самом деле IoC-контейнер — настолько дурацкое название, что навскидку даже сложно придумать что-то хуже. Оно совершенно ничего не говорит о том, чем на самом деле занимается, вводя в заблуждение десятки все новых и новых программистов каждый день. Абсолютно любой фреймворк можно назвать IoC-контейнером, так как он по определению реализует инверсию управления и является оболочкой для некоего кода общего назначения. Это термин был (и продолжает быть) настолько отвратительным, что Мартин Фаулер придумал другой — внедрение зависимости.
Подытожим. Мы используем Dependency Inversion, чтобы разделить модули абстракцией, Dependency Injection, чтобы избавиться от инстантинации вручную, а реализуем это посредством фреймворка, построенного по принципу Inversion of Control. И ничто из этого не является синонимами, поэтому IoC-контейнеры — яркий пример того, как можно намертво всех запутать с помощью одного единственного неудачного термина.
Надеюсь, в этом маленьком опусе у меня получилось донести до вас разницу между этими вещами, и вы больше никогда их не перепутаете. А если перепутает кто-то из ваших коллег, то вы сможете просто и понятно объяснить им, в чем они не правы.
Работа с IoC-контейнером в Spring
В этой статье рассказывается о шаблонах проектирования, на которых строится работа со Spring-контейнером и о том, каким классом он представлен во фреймворке Spring.
Мы создадим простейший пример, в котором конфигурируются бины, создается контейнер и из него извлекаются готовые бины.
Инверсия управления (Inversion of Control) – что это
Ключевая особенность приложения, написанного на Spring, состоит в том что большую часть объектов создаем не мы, а Spring. Мы лишь конфигурируем классы (с помощью аннотаций либо в конфигурационном XML), чтобы «объяснить» фреймворку Spring, какие именно объекты он должен создать за нас, и полями каких объектов их сделать. Spring управляет созданием объектов и потому его контейнер называется IoC-контейнер. IoC расшифровывается как Inversion of Control. А объекты, которые создаются контейнером и находятся под его управлением, называются бинами.
Иллюстрировать это можно так:
В общем на вход контейнер Spring принимает:
А на выходе он производит объекты – бины. То есть экземпляры классов, созданные в соответствии с конфигурацией и внедренные куда нужно (в другие бины). После этого никакие операторы new нам не понадобятся, мы будем работать в классе-бине с его полями-бинами так, будто они уже инициированы. Конечно, не со всеми полями, а только с теми, которые сконфигурированы как бины. Остальные инициализируются как обычно, в том числе с помощью оператора new.
Что такое внедрение зависимости (Dependency Injection)
Внедрение зависимости — это и есть инициализация полей бинов другими бинами (зависимостями).
Ведь помимо создания объектов, Spring-контейнер внедряет эти объекты в другие объекты, то есть делает их полями других объектов. Иногда это выглядит магически – например, контейнер способен внедрить зависимость в поле с модификатором private, для которого нет сеттера. Как же код Spring может это сделать? Дело в том, что под капотом он использует рефлексию, так что это реально. Но эти детали для нас как разработчиков не важны, главное знать, как объяснить фреймворку, какие объекты вы хотите отдать под его управление, и в какие поля других объектов вы хотите их внедрить.
Кстати, шаблон Dependency Injection не привязан к Spring, это всего лишь инициализация поля класса. Это такая обычная вещь и так часто встречается в коде (через конструктор либо сеттер), что даже странно выделять ее в отдельный шаблон. В связи со Spring это название мелькает часто наверно потому, что внедрение выполняет Spring, и у программиста тут много возни с конфигурацией зависимостей. Но с другой стороны, создание и внедрение Spring-ом — это уже другой шаблон — инверсия контроля (IoC).
Класс ApplicationContext для работы с IoC Контейнером
Конфигурация Maven
Чтобы иметь возможность работать с контейнером, добавьте в pom.xml зависимость:
Последнюю версию зависимости можно взять тут.
Класс ApplicationContext
Для работы с контейнером существует не один класс. Но удобнее всего работать с классом ApplicationContext. Чтобы инициализировать контейнер и создать в нем бины, нужно создать экземпляр класса ApplicationContext.
Как уже сказано, контейнеру для создания бинов. требуется конгфигурация, так что конструктор контейнера принимает аргумент. Существуют два подкласса ApplicationContext: ClassPathXmlApplicationContext берет конфигурацию из XML-файла, а AnnotationConfigApplicationContext – из аннотаций:
Так что контейнер можно создать, используя именно тот вид конфигурации, которая используется в вашем приложении. Обычно это аннотации, XML немного устарел.
В конфигурации прописаны как исходные классы, которые надо сделать бинами, так и их зависимости, а также каким образом внедрить эти зависимости. Обычно зависимость внедряется либо через конструктор, либо через сеттер, в зависимости от дизайна класса.
После того, как мы инициализировали контейнер, и он создал бины, появляется возможность их получить непосредственно из контейнера. Хотя это не всегда нужно, но можно. Делается это так:
Мы получили бин типа Animal из контейнера в предположении, что он сконфигурирован в нашем приложении. Давайте теперь его на самом деле сконфигурируем.
Конфигурация бинов с помощью аннотаций
Задавать конфигурацию будем с помощью аннотаций, поскольку это более современный и удобный способ.
Во-первых, создадим класс конфигурации и аннотируем его с помощью @Configuration:
Во-вторых, допустим, у нас есть класс Animal, который мы хотим делать бином:
Чтобы сделать Animal бином, создадим в классе конфигурации метод,который создает и возвращает Animal, и аннотируем этот метод с помощью @Bean:
Все, бин сконфигурирован. Теперь экземпляр Animal будет создаваться контейнером автоматически, и мы сможем получить его из контейнера, как показано выше.
Возможно вы спросите, в чем смысл этого, ведь все равно мы создаем экземпляр Animal отдельным методом. Где же польза контейнера?
Да, мы прописываем метод, создающий Animal. Но мы не вызываем этот метод. Мы просто создаем контейнер. А метод вызывается контейнером во время его инициализации. И учитывая то, что бины обычно конфигурируются однотипно, выгода есть. К тому же они обычно создаются контейнером в единственном экземпляре (хотя это зависит от конфигурации) и внедряются в поля других бинов согласно конфигурации автоматически.
Причем вместо создания аннотированного @Bean метода, можно аннотировать класс Animal изнутри аннотацией @Component – а это и вовсе одна строчка.
О том, как сконфигурировать внедрение бинов в поля других бинов, читайте статью про внедрение зависимостей.
Разница между аннотациями @Bean и @Component в том, что @Bean более гибкая аннотация, ею мы аннотируем метод, а не класс:
Давайте сконфигурируем второй бин Man с помощью аннотации @Component:
Только учтите, что если мы аннотируем классы с помощью @Component, то в файл конфигурации надо добавить аннотацию @ComponentScan для автоматического поиска этих аннотированных классов. В ней надо указать имя пакета или нескольких пакетов, в который лежат эти классы. В этом пакете контейнер будет пытаться их найти автоматически.
Давайте добавим в файл конфигурации одну строчку с аннотацией @ComponentScan и именем пакета для поиска бинов:
Все бины, аннотированные с помощью @ComponentScan, должны лежать в пакете «ru.javalang.ioc».
Проверим, что оба бина действительно создаются:
Мы рассмотрели, что такое IoC-контейнер, как его создать и как из него получить бины. Также мы узнали, что можно сконфигурировать бины с помощью аннотаций @Bean и @Component.
Исходый код примера для этой статьи доступен на GitHub.