
Corso di Java Spring Framework #8 | Modalità di Dependency Injection
In questo nuovo articolo vedremo le diverse modalità di Depedency Injection implementate a livello del framework.
Partiamo dicendo che questa funzionalità viene gestita mediante l'annotazione @Autowired
, specifica di Spring, che svolge un compito simile alla @Inject
che fa parte delle specifiche JSR.
Tale annotazione va collocata in punti particolari del codice, detti injection point, dove verrà effettuata appunto l'iniezione delle dipendenze.
Esistono tre diverse modalità: field injection, constructor injection e setter injection.
Partiamo dalla prima, la più semplice, che consiste nell'aggiungere l'annotazione @Autowired
in corrispondenza di un attributo (field) della classe che rappresenta la dipendenza che vogliamo inserire all'interno del nostro componente.
@Component
public class Car {
@Autowired
private Engine engine;
public Car() {
}
public Engine getEngine() {
return engine;
}
public void start() {
engine.turnOn();
}
}
In parole povere l'annotazione dice a Spring di ricercare un bean di quel tipo, in quanto di default l'injection viene effettuata by-type, anche se è possibile adottare la modalità by-name ovvero indicando il nome di uno specifico bean.
Non dovendo aggiungere altro codice questa può sembrare la soluzione ottimale, ma in realtà la field injection è deprecata e di fatto riservata a scopi didattici.
Ma come funziona esattamente l'injection? Come fa Spring ad accedere a field che normalmente non sono pubblici per motivi di sicurezza?
La parola chiave è "reflection" che sostanzialmente consente di esaminare e modificare la struttura di una classe a runtime.
Quindi senza la reflection non è possibile effettuare l'injection e di fatto si crea un accoppiamento implicito con il framework.
Il secondo problema è legato all'impossibilità di rendere questi field immutabili: non li possiamo marcare come final
perché l'inizializzazione andrebbe fatta necessariamente in corrispondenza della creazione della classe e non in un secondo momento.
Questo è un problema dal punto di vista della sicurezza perché in genere rendere immutabili queste dipendenze fa sì che il loro valore non possa più essere modificato.
Il terzo problema è relativo al testing, in particolare ai test unitari, dove è necessario testare il codice in isolamento rispetto al resto dell'applicazione. In questo caso si ricorre ai cosiddetti mock, oggetti che simulano il comportamento di altri componenti o di servizi esterni.
Di fatto diventa difficile se non impossibile fare dei test senza ricorrere a Spring per la questione della reflection di cui parlavamo in precedenza.
Inoltre non c'è alcun controllo sul numero di dipendenze che è possibile aggiungere ad una classe, con il rischio di aggiungerne delle inutili appesantendo l'applicazione e sprecando preziose risorse.
Ricordiamoci sempre che la dependency injection non è a costo zero e va utilizzata in modo oculato, ovvero dove è realmente utile.
Parlando in seguito degli scope dei bean capiremo ancora meglio questo problema, ovvero la presenza di molti punti di injection amplia la superficie di attacco e aumenta il rischio di modifiche indsiderate con ripercussioni sulla sicurezza dell'applicazione.
Per tutti questi motivi è stata introdotta la Constructor Injection, ovvero l'iniezione delle dipendenze attraverso il costruttore.
In questo modo andiamo ad elencare le dipendenze come argomenti e annotiamo il costruttore con @Autowired
.
@Component
public class Car {
private Engine engine;
public Car() {
}
@Autowired
public Car(Engine engine) {
this.engine = engine;
}
public Engine getEngine() {
return engine;
}
public void start() {
engine.turnOn();
}
}
La differenza principale rispetto al precedente approccio è che le dipendenze sono tutte specificate all'interno del costruttore, quindi ogni modifica avrà impatto sullo stesso rendendoci consapevoli di ciò che stiamo facendo.
Anche la presenza di un numero elevato di parametri può indicarci qualche problema di design, magari una violazione del principio SOLID della singola responsabilità.
Inoltre possiamo rendere tali dipendenze immutabili marcandole come final
, poiché l'inizializzazione deve essere fatta contestualmente alla creazione dell'istanza della classe.
Questo ci consente anche di evitare i famigerati NullPointerException
che con la field injection potrebbero verificarsi a runtime per una non corretta inizializzazione.
Occore precisare che a partire dalla versione 4.3 di Spring si può omettere l'annotazione @Autowired
in corrispondenza del costruttore se é l'unico all'interno della classe.
In quel caso tutte le dipendenze obbligatorie saranno immediatamente individuabili e inserite automaticamente.
In presenza di più costruttori uno di essi dovrà riportare necessariamente l'annotazione, altrimenti Spring non saprà quale prendere in considerazione per l'injection delle dipendenze, sollevando un'eccezione.
L'ultima modalità si ricollega al fatto che con il costruttore tutte le dipendenze sono obbligatorie mentre è possibile che alcune di esse siano opzionali, cioè che possano essere iniettate o meno a seconda del contesto in cui ci troviamo e del flusso di esecuzione dell'applicazione.
La soluzione consiste nell'applicare @Autowired
in corrispondenza di un metodo setter specificando eventualmente required=false
se è possibile che la dipendenza non venga risolta. In questo modo si evita che Spring lanci un'eccezione a runtime visto che di default tutte le dipendenze sono richieste.
@Component
public class Car {
private Engine engine;
public Car() {
}
public Engine getEngine() {
return engine;
}
@Autowired // @Autowired(required=false) se opzionale
public void setEngine(Engine engine) {
this.engine = engine;
}
public void start() {
engine.turnOn();
}
}
Così se abbiamo bisogno di iniettare delle dipendenze in un momento specifico possiamo farlo con la chiamata al metodo setter.
Riepilogando quanto visto finora possiamo dire che le best practices suggeriscono di utilizzare principalmente o forse essenzialmente la constructor injection, laddove sia necessario specificare le dipendenze obbligatorie, e riservare alla setter injection quelle opzionali.
La field injection andrebbe relegata a scopi didattici per capirne i meccanismi di funzionamento, evitando il suo impiego in produzione per le problematiche viste in precedenza.
[VIDEO YOUTUBE]
[LINKS]
`