1. Общ преглед
В тази статия ще представим понятията IoC (инверсия на контрола) и DI (инжектиране на зависимост) и след това ще разгледаме как те се прилагат в рамката на Spring.
2. Какво представлява инверсията на контрола?
Inversion of Control е принцип в софтуерното инженерство, чрез който управлението на обекти или части от програма се прехвърля в контейнер или рамка. Най-често се използва в контекста на обектно-ориентираното програмиране.
За разлика от традиционното програмиране, при което нашият персонализиран код осъществява повиквания към библиотека, IoC дава възможност на рамка да поеме контрола върху потока на програмата и да извършва повиквания към нашия потребителски код. За да се активира това, рамките използват абстракции с вградено допълнително поведение. Ако искаме да добавим наше собствено поведение, трябва да разширим класовете на рамката или да добавим нашите собствени класове.
Предимствата на тази архитектура са:
- отделяне на изпълнението на задача от нейното изпълнение
- което улеснява превключването между различни изпълнения
- по-голяма модулност на програмата
- по-голяма лекота при тестване на програма чрез изолиране на компонент или подигравка на неговите зависимости и позволяване на компонентите да комуникират чрез договори
Инверсията на контрола може да бъде постигната чрез различни механизми като: модел на проектиране на стратегия, модел на локатор на услуги, фабричен модел и инжектиране на зависимост (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, базиран на сетер, контейнерът ще извика методи за задаване от нашия клас, след като извика конструктор без аргумент или статичен фабричен метод без аргумент, за да създаде екземпляр на боб. Нека създадем тази конфигурация, като използваме пояснения:
@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:
- Обръщане на контролни контейнери и модел на инжектиране на зависимост.
- Обръщане на контрол
И можете да научите повече за пролетните внедрения на IoC и DI в Spring Framework Справочна документация.