Wprowadzenie do inwersji kontroli i iniekcji zależności w Spring

1. Przegląd

W tym artykule przedstawimy koncepcje IoC (Inversion of Control) i DI (Dependency Injection), a następnie przyjrzymy się, jak są one implementowane we frameworku Spring.

2. Co to jest odwrócenie kontroli?

Odwrócenie kontroli to zasada w inżynierii oprogramowania, dzięki której kontrola obiektów lub części programu jest przenoszona do kontenera lub struktury. Jest najczęściej używany w kontekście programowania obiektowego.

W przeciwieństwie do tradycyjnego programowania, w którym nasz niestandardowy kod wywołuje bibliotekę, IoC umożliwia platformie przejęcie kontroli nad przepływem programu i wywoływanie naszego kodu niestandardowego. Aby to umożliwić, frameworki używają abstrakcji z wbudowanymi dodatkowymi zachowaniami. Jeśli chcemy dodać własne zachowanie, musimy rozszerzyć klasy frameworka lub podłączyć własne.

Zalety tej architektury to:

  • oddzielenie wykonania zadania od jego realizacji
  • ułatwiając przełączanie się między różnymi implementacjami
  • większa modułowość programu
  • większa łatwość testowania programu poprzez izolowanie komponentu lub mockowanie jego zależności i zezwalanie komponentom na komunikację poprzez kontrakty

Odwrócenie kontroli można osiągnąć za pomocą różnych mechanizmów, takich jak: wzorzec projektowania strategii, wzorzec lokalizatora usług, wzorzec fabryki i wstrzyknięcie zależności (DI).

W następnej kolejności przyjrzymy się DI.

3. Co to jest iniekcja zależności?

Wstrzykiwanie zależności to wzorzec, za pomocą którego można zaimplementować IoC, w którym odwrócona kontrola to ustawienie zależności obiektu.

Czynność łączenia obiektów z innymi obiektami lub „wstrzykiwanie” obiektów do innych obiektów jest wykonywana raczej przez asemblera niż przez same obiekty.

Oto, jak utworzyć zależność od obiektu w tradycyjnym programowaniu:

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

W powyższym przykładzie musimy utworzyć instancję interfejsu Item w samej klasie Store .

Korzystając z DI, możemy przepisać przykład bez określania implementacji elementu , który chcemy:

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

W następnych sekcjach zobaczymy, jak możemy zapewnić implementację elementu za pomocą metadanych.

Zarówno IoC, jak i DI to proste koncepcje, ale mają głębokie implikacje w sposobie, w jaki tworzymy nasze systemy, więc warto je dobrze zrozumieć.

4. Pojemnik wiosennego IoC

Kontener IoC jest typową cechą platform implementujących IoC.

W strukturze Spring kontener IoC jest reprezentowany przez interfejs ApplicationContext . Kontener Spring jest odpowiedzialny za tworzenie instancji, konfigurowanie i składanie obiektów zwanych fasolami , a także za zarządzanie ich cyklem życia.

Struktura Spring udostępnia kilka implementacji interfejsu ApplicationContext - ClassPathXmlApplicationContext i FileSystemXmlApplicationContext dla aplikacji autonomicznych oraz WebApplicationContext dla aplikacji internetowych.

W celu złożenia bean kontener wykorzystuje metadane konfiguracyjne, które mogą mieć postać konfiguracji XML lub adnotacji.

Oto jeden sposób na ręczne utworzenie wystąpienia kontenera:

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

Aby ustawić atrybut pozycji w powyższym przykładzie, możemy użyć metadanych. Następnie kontener odczyta te metadane i użyje ich do złożenia komponentów bean w czasie wykonywania.

Dependency Injection w Spring można wykonać za pomocą konstruktorów, ustawiaczy lub pól.

5. Wstrzykiwanie zależności na podstawie konstruktora

W przypadku iniekcji zależności opartej na konstruktorze kontener wywoła konstruktor z argumentami, z których każdy reprezentuje zależność, którą chcemy ustawić.

Spring rozwiązuje każdy argument głównie według typu, po którym następuje nazwa atrybutu i indeks w celu ujednoznacznienia. Zobaczmy konfigurację fasoli i jej zależności za pomocą adnotacji:

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

@Configuration adnotacja oznacza, że klasa jest źródłem definicji fasoli. Możemy również dodać go do wielu klas konfiguracyjnych.

@Bean Adnotacja jest stosowany w metodzie do określenia fasoli. Jeśli nie określimy niestandardowej nazwy, nazwa ziarna będzie domyślnie odpowiadać nazwie metody.

W przypadku komponentu bean z domyślnym zakresem singleton Spring najpierw sprawdza, czy buforowana instancja komponentu bean już istnieje i tworzy nową tylko wtedy, gdy tak nie jest. Jeśli używamy zakresu prototypu , kontener zwraca nową instancję komponentu bean dla każdego wywołania metody.

Innym sposobem tworzenia konfiguracji komponentów bean jest konfiguracja XML:

6. Wstrzykiwanie zależności oparte na ustawieniach

W przypadku DI opartego na metodzie ustawiającej kontener wywoła metody ustawiające naszej klasy po wywołaniu bezargumentowego konstruktora lub statycznej metody fabrycznej bez argumentów w celu utworzenia wystąpienia komponentu bean. Utwórzmy tę konfigurację za pomocą adnotacji:

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

Możemy również użyć XML dla tej samej konfiguracji fasoli:

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:

  • Odwrócenie kontenerów sterujących i wzorzec wstrzykiwania zależności.
  • Odwrócenie sterowania

Więcej informacji na temat implementacji IoC i DI w Spring można znaleźć w dokumentacji referencyjnej Spring Framework.