Вступ до інверсії контролю та впорскування залежності з пружиною

1. Огляд

У цій статті ми представимо поняття IoC (інверсія контролю) та DI (ін’єкція залежності), а потім поглянемо, як вони реалізовані в рамках Spring.

2. Що таке інверсія контролю?

Інверсія управління - це принцип у програмній інженерії, за допомогою якого управління об’єктами або частинами програми передається в контейнер або фреймворк. Найчастіше він використовується в контексті об’єктно-орієнтованого програмування.

На відміну від традиційного програмування, коли наш користувацький код здійснює виклики до бібліотеки, IoC дає можливість структурі, яка контролює потік програми та здійснює виклики до нашого користувацького коду. Щоб увімкнути це, фреймворки використовують абстракції із вбудованою додатковою поведінкою. Якщо ми хочемо додати свою власну поведінку, нам потрібно розширити класи фреймворку або вставити наші власні класи.

Перевагами цієї архітектури є:

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

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

Далі ми розглянемо DI.

3. Що таке ін’єкція залежності?

Введення залежності - це шаблон, за допомогою якого можна реалізувати IoC, де інвертований елемент керування є встановленням залежностей об’єкта.

Акт з'єднання об'єктів з іншими об'єктами або "впорскування" об'єктів в інші об'єкти здійснюється асемблером, а не самими об'єктами.

Ось як ви могли б створити об’єктну залежність у традиційному програмуванні:

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

У наведеному вище прикладі нам потрібно створити екземпляр реалізації інтерфейсу Item у самому класі Store .

При використанні DI, ми можемо переписати приклад без вказівки реалізації пункту , що ми хочемо:

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

У наступних розділах ми побачимо, як ми можемо забезпечити реалізацію елемента за допомогою метаданих.

І IoC, і DI - це прості поняття, але вони мають глибокі наслідки в способі структури нашої системи, тому їх варто добре розуміти.

4. Весняний контейнер IoC

IoC-контейнер є загальною характеристикою фреймворків, що реалізують IoC.

У структурі Spring контейнер IoC представлений інтерфейсом ApplicationContext . Контейнер Spring відповідає за створення, налаштування та складання об'єктів, відомих як " компоненти" , а також управління їх життєвим циклом.

Структура Spring забезпечує кілька реалізацій інтерфейсу ApplicationContext - ClassPathXmlApplicationContext та FileSystemXmlApplicationContext для автономних програм та WebApplicationContext для веб-додатків.

Для складання компонентів контейнер використовує метадані конфігурації, які можуть бути у формі конфігурації XML або анотацій.

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

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

Щоб встановити атрибут item у наведеному вище прикладі, ми можемо використовувати метадані. Потім контейнер прочитає ці метадані та використає їх для збирання бобів під час виконання.

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

5. Інжекція залежностей на основі конструктора

У випадку введення залежностей на основі конструктора контейнер буде викликати конструктор з аргументами, кожен з яких представляє залежність, яку ми хочемо встановити.

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

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

@Configuration анотацію вказує на те, що клас є джерелом бінов. Крім того, ми можемо додати його до декількох класів конфігурації.

@Bean анотацій використовується на методі визначення боб. Якщо ми не вказали власне ім'я, ім'я компонента буде за замовчуванням назвою методу.

Для квасолі з за замовчуванням одноплодной рамки, Spring спочатку перевіряє , якщо в кеші примірник компоненти вже існує і тільки створює новий, якщо він не робить. Якщо ми використовуємо область прототипу , контейнер повертає новий екземпляр компонента для кожного виклику методу.

Інший спосіб створити конфігурацію компонентів - це конфігурація XML:

6. Ін’єкція залежностей на основі сеттера

Для DI на основі сеттера контейнер буде викликати методи встановлення нашого класу, після виклику конструктора no-argument або статичного фабричного методу no-argument для створення екземпляра компонента. Давайте створимо цю конфігурацію за допомогою анотацій:

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

Ми також можемо використовувати XML для тієї ж конфігурації бобів:

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.