Introduzione all'inversione di controllo e iniezione di dipendenza con Spring

1. Panoramica

In questo articolo, introdurremo i concetti di IoC (Inversion of Control) e DI (Dependency Injection), quindi daremo un'occhiata a come questi sono implementati nel framework Spring.

2. Che cos'è l'inversione del controllo?

L'inversione del controllo è un principio nell'ingegneria del software mediante il quale il controllo di oggetti o parti di un programma viene trasferito a un contenitore o framework. Viene spesso utilizzato nel contesto della programmazione orientata agli oggetti.

A differenza della programmazione tradizionale, in cui il nostro codice personalizzato effettua chiamate a una libreria, IoC consente a un framework di assumere il controllo del flusso di un programma ed effettuare chiamate al nostro codice personalizzato. Per abilitare ciò, i framework usano astrazioni con un comportamento aggiuntivo incorporato. Se vogliamo aggiungere il nostro comportamento, dobbiamo estendere le classi del framework o inserire le nostre classi.

I vantaggi di questa architettura sono:

  • disaccoppiare l'esecuzione di un'attività dalla sua implementazione
  • rendendo più semplice il passaggio tra diverse implementazioni
  • maggiore modularità di un programma
  • maggiore facilità nel testare un programma isolando un componente o deridendone le dipendenze e consentendo ai componenti di comunicare tramite contratti

L'inversione del controllo può essere ottenuta attraverso vari meccanismi come: modello di progettazione della strategia, modello di localizzazione del servizio, modello di fabbrica e iniezione di dipendenza (DI).

Daremo un'occhiata al DI dopo.

3. Che cos'è l'inserimento delle dipendenze?

L'inserimento delle dipendenze è un modello attraverso il quale implementare IoC, in cui il controllo invertito è l'impostazione delle dipendenze dell'oggetto.

L'atto di connettere oggetti con altri oggetti, o "iniettare" oggetti in altri oggetti, è svolto da un assemblatore piuttosto che dagli oggetti stessi.

Ecco come creeresti una dipendenza da oggetti nella programmazione tradizionale:

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

Nell'esempio sopra, abbiamo bisogno di istanziare un'implementazione dell'interfaccia Item all'interno della stessa classe Store .

Utilizzando DI, possiamo riscrivere l'esempio senza specificare l'implementazione di Item che vogliamo:

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

Nelle prossime sezioni vedremo come possiamo fornire l'implementazione di Item attraverso i metadati.

Sia IoC che DI sono concetti semplici, ma hanno profonde implicazioni nel modo in cui strutturiamo i nostri sistemi, quindi vale la pena comprenderli bene.

4. Il contenitore Spring IoC

Un contenitore IoC è una caratteristica comune dei framework che implementano IoC.

Nel framework Spring, il contenitore IoC è rappresentato dall'interfaccia ApplicationContext . Il contenitore Spring è responsabile della creazione di istanze, configurazione e assemblaggio di oggetti noti come bean , nonché della gestione del loro ciclo di vita.

Il framework Spring fornisce diverse implementazioni dell'interfaccia ApplicationContext : ClassPathXmlApplicationContext e FileSystemXmlApplicationContext per le applicazioni autonome e WebApplicationContext per le applicazioni web.

Per assemblare i bean, il contenitore utilizza i metadati di configurazione, che possono essere sotto forma di configurazione XML o annotazioni.

Ecco un modo per creare manualmente un'istanza di un contenitore:

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

Per impostare l' attributo dell'articolo nell'esempio sopra, possiamo utilizzare i metadati. Quindi, il contenitore leggerà questi metadati e li utilizzerà per assemblare i bean in fase di esecuzione.

L'iniezione di dipendenza in primavera può essere eseguita tramite costruttori, setter o campi.

5. Iniezione delle dipendenze basata sul costruttore

Nel caso dell'inserimento delle dipendenze basato sul costruttore, il contenitore richiamerà un costruttore con argomenti ciascuno che rappresenta una dipendenza che vogliamo impostare.

Spring risolve ogni argomento principalmente per tipo, seguito dal nome dell'attributo e dall'indice per la disambiguazione. Vediamo la configurazione di un bean e le sue dipendenze utilizzando le annotazioni:

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

L' annotazione @Configuration indica che la classe è un'origine delle definizioni dei bean. Inoltre, possiamo aggiungerlo a più classi di configurazione.

L' annotazione @Bean viene utilizzata su un metodo per definire un bean. Se non specifichiamo un nome personalizzato, il nome del bean sarà predefinito sul nome del metodo.

Per un bean con l' ambito singleton predefinito , Spring controlla prima se un'istanza memorizzata nella cache del bean esiste già e ne crea una nuova solo in caso contrario. Se stiamo usando l' ambito del prototipo , il contenitore restituisce una nuova istanza di bean per ogni chiamata al metodo.

Un altro modo per creare la configurazione dei bean è tramite la configurazione XML:

6. Iniezione di dipendenza basata su setter

Per DI basata su setter, il contenitore chiamerà i metodi setter della nostra classe, dopo aver richiamato un costruttore senza argomenti o un metodo factory statico senza argomenti per istanziare il bean. Creiamo questa configurazione utilizzando le annotazioni:

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

Possiamo anche usare XML per la stessa configurazione di 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:

  • Inversione dei contenitori di controllo e pattern di iniezione delle dipendenze.
  • Inversione di controllo

E puoi saperne di più sulle implementazioni Spring di IoC e DI nella documentazione di riferimento del framework Spring.