Introducere în inversiunea controlului și a injecției de dependență cu primăvara

1. Prezentare generală

În acest articol, vom introduce conceptele IoC (Inversion of Control) și DI (Dependency Injection) și vom analiza apoi modul în care acestea sunt implementate în cadrul Spring.

2. Ce este inversiunea controlului?

Inversiunea controlului este un principiu în ingineria software prin care controlul obiectelor sau porțiunilor unui program este transferat într-un container sau cadru. Este cel mai des folosit în contextul programării orientate pe obiecte.

Spre deosebire de programarea tradițională, în care codul nostru personalizat efectuează apeluri către o bibliotecă, IoC permite unui cadru să preia controlul asupra fluxului unui program și să efectueze apeluri către codul nostru personalizat. Pentru a activa acest lucru, cadrele folosesc abstracții cu comportament suplimentar încorporat. Dacă dorim să adăugăm propriul comportament, trebuie să extindem clasele cadrului sau să conectăm propriile noastre clase.

Avantajele acestei arhitecturi sunt:

  • decuplarea executării unei sarcini de implementarea acesteia
  • facilitând trecerea între diferite implementări
  • o mai mare modularitate a unui program
  • o ușurință mai mare în testarea unui program prin izolarea unei componente sau batjocorirea dependențelor sale și permiterea componentelor să comunice prin contracte

Inversiunea controlului poate fi realizată prin diferite mecanisme, cum ar fi: modelul de proiectare a strategiei, modelul de localizare a serviciilor, modelul din fabrică și injecția de dependență (DI).

Urmează să ne uităm la DI.

3. Ce este injecția de dependență?

Injecția de dependență este un model prin care să implementăm IoC, unde controlul inversat este setarea dependențelor obiectului.

Actul de a conecta obiecte cu alte obiecte sau de a „injecta” obiecte în alte obiecte este realizat de un asamblator mai degrabă decât de obiectele în sine.

Iată cum ați crea o dependență de obiect în programarea tradițională:

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

În exemplul de mai sus, trebuie să instanțiem o implementare a interfeței Item în cadrul clasei Store .

Folosind DI, putem rescrie exemplul fără a specifica implementarea elementului pe care îl dorim:

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

În secțiunile următoare, vom vedea cum putem furniza implementarea Itemului prin metadate.

Atât IoC, cât și DI sunt concepte simple, dar au implicații profunde în modul în care ne structurăm sistemele, deci merită să le înțelegem bine.

4. Containerul IoC de primăvară

Un container IoC este o caracteristică comună a cadrelor care implementează IoC.

În cadrul Spring, containerul IoC este reprezentat de interfața ApplicationContext . Containerul Spring este responsabil pentru instanțierea, configurarea și asamblarea obiectelor cunoscute sub denumirea de fasole , precum și gestionarea ciclului lor de viață.

Cadrul Spring oferă mai multe implementări ale interfeței ApplicationContext - ClassPathXmlApplicationContext și FileSystemXmlApplicationContext pentru aplicații independente și WebApplicationContext pentru aplicații web.

Pentru a asambla fasole, containerul folosește metadate de configurare, care pot fi sub formă de configurație XML sau adnotări.

Iată o modalitate de a instanția manual un container:

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

Pentru a seta atributul articolului în exemplul de mai sus, putem folosi metadatele. Apoi, containerul va citi aceste metadate și le va folosi pentru a asambla fasole în timpul rulării.

Injectarea dependenței în primăvară se poate face prin constructori, setere sau câmpuri.

5. Injecție de dependență bazată pe constructori

În cazul injecției de dependență bazată pe constructor, containerul va invoca un constructor cu argumente care reprezintă fiecare o dependență pe care dorim să o setăm.

Spring rezolvă fiecare argument în primul rând după tip, urmat de numele atributului și indexul pentru dezambiguizare. Să vedem configurația unui bean și dependențele sale folosind adnotări:

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

@Configuration adnotarea indică faptul că clasa este o sursă de definiții de fasole. De asemenea, îl putem adăuga la mai multe clase de configurare.

@Bean adnotare este utilizat pe o metodă pentru a defini o fasole. Dacă nu specificăm un nume personalizat, numele bean va fi implicit numele metodei.

Pentru un bean cu sfera de implicire singleton , Spring verifică mai întâi dacă există deja o instanță cache a bean și creează una nouă doar dacă nu există. Dacă folosim domeniul prototip , containerul returnează o nouă instanță de fasole pentru fiecare apel de metodă.

O altă modalitate de a crea configurația de fasole este prin configurarea XML:

6. Injecție de dependență bazată pe setter

Pentru DI bazat pe setter, containerul va apela metodele setter din clasa noastră, după ce a invocat un constructor fără argumente sau o metodă de fabrică statică fără argumente pentru a instanția beanul. Să creăm această configurație folosind adnotări:

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

De asemenea, putem folosi XML pentru aceeași configurație a beans:

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:

  • Inversia containerelor de control și modelul de injecție de dependență.
  • Inversarea controlului

Și puteți afla mai multe despre implementările de primăvară ale IoC și DI în Documentația de referință cadru de primăvară.