Введение в инверсию управления и внедрение зависимостей с помощью Spring

1. Обзор

В этой статье мы познакомим вас с концепциями IoC (инверсия управления) и DI (внедрение зависимостей), а затем мы рассмотрим, как они реализованы в среде Spring.

2. Что такое инверсия управления?

Инверсия управления - это принцип разработки программного обеспечения, согласно которому управление объектами или частями программы передается контейнеру или фреймворку. Чаще всего используется в контексте объектно-ориентированного программирования.

В отличие от традиционного программирования, при котором наш пользовательский код выполняет вызовы библиотеки, IoC позволяет фреймворку управлять потоком программы и выполнять вызовы нашего пользовательского кода. Для этого фреймворки используют абстракции со встроенным дополнительным поведением. Если мы хотим добавить собственное поведение, нам нужно расширить классы фреймворка или добавить наши собственные классы.

Преимущества этой архитектуры:

  • отделение выполнения задачи от ее реализации
  • упрощая переключение между различными реализациями
  • большая модульность программы
  • большая легкость тестирования программы за счет изоляции компонента или имитации его зависимостей и разрешения компонентам обмениваться данными через контракты

Инверсия управления может быть достигнута с помощью различных механизмов, таких как: шаблон разработки стратегии, шаблон Service Locator, шаблон Factory и внедрение зависимостей (DI).

Далее мы рассмотрим DI.

3. Что такое внедрение зависимостей?

Внедрение зависимостей - это шаблон для реализации IoC, где инвертируемый элемент управления - это установка зависимостей объекта.

Акт соединения объектов с другими объектами или «инъекции» объектов в другие объекты выполняется ассемблером, а не самими объектами.

Вот как можно создать объектную зависимость в традиционном программировании:

public class Store { private Item item; public Store() { item = new ItemImpl1(); } }

В приведенном выше примере нам нужно создать экземпляр реализации интерфейса Item в самом классе Store .

Используя DI, мы можем переписать пример, не указывая реализацию Item, которую мы хотим:

public class Store { private Item item; public Store(Item item) { this.item = item; } }

В следующих разделах мы увидим, как мы можем обеспечить реализацию Item через метаданные.

И IoC, и DI - это простые концепции, но они имеют глубокое значение в том, как мы структурируем наши системы, поэтому их стоит хорошо понять.

4. Контейнер Spring IoC

Контейнер IoC - это общая характеристика фреймворков, реализующих IoC.

В среде Spring контейнер IoC представлен интерфейсом ApplicationContext . Контейнер Spring отвечает за создание экземпляров, настройку и сборку объектов, известных как beans , а также за управление их жизненным циклом.

Платформа Spring предоставляет несколько реализаций интерфейса ApplicationContext - ClassPathXmlApplicationContext и FileSystemXmlApplicationContext для автономных приложений и WebApplicationContext для веб-приложений.

Для сборки bean-компонентов контейнер использует метаданные конфигурации, которые могут быть в форме XML-конфигурации или аннотаций.

Вот один из способов вручную создать экземпляр контейнера:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

Чтобы установить атрибут элемента в приведенном выше примере, мы можем использовать метаданные. Затем контейнер считывает эти метаданные и использует их для сборки bean-компонентов во время выполнения.

Внедрение зависимостей в Spring может выполняться через конструкторы, сеттеры или поля.

5. Внедрение зависимостей на основе конструкторов

В случае внедрения зависимости на основе конструктора контейнер вызовет конструктор с аргументами, каждый из которых представляет зависимость, которую мы хотим установить.

Spring разрешает каждый аргумент в первую очередь по типу, за которым следуют имя атрибута и индекс для устранения неоднозначности. Давайте посмотрим на конфигурацию bean-компонента и его зависимости с помощью аннотаций:

@Configuration public class AppConfig { @Bean public Item item1() { return new ItemImpl1(); } @Bean public Store store() { return new Store(item1()); } }

@Configuration аннотацию указывает на то, что класс является источником бинов. Кроме того, мы можем добавить его в несколько классов конфигурации.

@Bean аннотаций используется на методе определения боб. Если мы не укажем настраиваемое имя, имя компонента по умолчанию будет соответствовать имени метода.

Для bean-компонента с одноэлементной областью действия по умолчанию Spring сначала проверяет, существует ли уже кэшированный экземпляр bean-компонента, и создает новый только в том случае, если это не так. Если мы используем область видимости прототипа , контейнер возвращает новый экземпляр компонента для каждого вызова метода.

Другой способ создать конфигурацию bean-компонентов - через конфигурацию XML:

6. Внедрение зависимостей на основе установщика

Для DI на основе установщика контейнер будет вызывать методы установщика нашего класса после вызова конструктора без аргументов или статического фабричного метода без аргументов для создания экземпляра bean-компонента. Создадим эту конфигурацию, используя аннотации:

@Bean public Store store() { Store store = new Store(); store.setItem(item1()); return store; }

Мы также можем использовать XML для той же конфигурации bean-компонентов:

Constructor-based and setter-based types of injection can be combined for the same bean. The Spring documentation recommends using constructor-based injection for mandatory dependencies, and setter-based injection for optional ones.

7. Field-Based Dependency Injection

In case of Field-Based DI, we can inject the dependencies by marking them with an @Autowired annotation:

public class Store { @Autowired private Item item; }

While constructing the Store object, if there's no constructor or setter method to inject the Item bean, the container will use reflection to inject Item into Store.

We can also achieve this using XML configuration.

This approach might look simpler and cleaner but is not recommended to use because it has a few drawbacks such as:

  • This method uses reflection to inject the dependencies, which is costlier than constructor-based or setter-based injection
  • It's really easy to keep adding multiple dependencies using this approach. If you were using constructor injection having multiple arguments would have made us think that the class does more than one thing which can violate the Single Responsibility Principle.

More information on @Autowired annotation can be found in Wiring In Spring article.

8. Autowiring Dependencies

Wiring allows the Spring container to automatically resolve dependencies between collaborating beans by inspecting the beans that have been defined.

There are four modes of autowiring a bean using an XML configuration:

  • no: the default value – this means no autowiring is used for the bean and we have to explicitly name the dependencies
  • byName: autowiring is done based on the name of the property, therefore Spring will look for a bean with the same name as the property that needs to be set
  • byType: similar to the byName autowiring, only based on the type of the property. This means Spring will look for a bean with the same type of the property to set. If there's more than one bean of that type, the framework throws an exception.
  • constructor: autowiring is done based on constructor arguments, meaning Spring will look for beans with the same type as the constructor arguments

For example, let's autowire the item1 bean defined above by type into the store bean:

@Bean(autowire = Autowire.BY_TYPE) public class Store { private Item item; public setItem(Item item){ this.item = item; } }

We can also inject beans using the @Autowired annotation for autowiring by type:

public class Store { @Autowired private Item item; }

If there's more than one bean of the same type, we can use the @Qualifier annotation to reference a bean by name:

public class Store { @Autowired @Qualifier("item1") private Item item; }

Now, let's autowire beans by type through XML configuration:

Next, let's inject a bean named item into the item property of store bean by name through XML:

We can also override the autowiring by defining dependencies explicitly through constructor arguments or setters.

9. Lazy Initialized Beans

By default, the container creates and configures all singleton beans during initialization. To avoid this, you can use the lazy-init attribute with value true on the bean configuration:

As a consequence, the item1 bean will be initialized only when it's first requested, and not at startup. The advantage of this is faster initialization time, but the trade-off is that configuration errors may be discovered only after the bean is requested, which could be several hours or even days after the application has already been running.

10. Conclusion

In this article, we've presented the concepts of inversion of control and dependency injection and exemplified them in the Spring framework.

You can read more about these concepts in Martin Fowler's articles:

  • Инверсия управляющих контейнеров и шаблон внедрения зависимостей.
  • Инверсия контроля

И вы можете узнать больше о реализациях Spring IoC и DI в справочной документации Spring Framework.