Google Guice è una libreria Java realizzata e utilizzata a lungo su progetti interni dal gigante di Mountain View, poi rilasciata con licenza Apache. Si tratta di un container alla stregua di Spring che sfrutta appieno le caratteristiche introdotte di Java 1.5 come le annotation, per associare dati di configurazione direttamente a porzioni di codice, invece che ricorrere a lunghi file XML e i generics per gestire i tipi in maniera flessibile.
L'obiettivo di questo articolo è fornire una panoramica introduttiva sulle funzioni principali fra quelle fornite da Guice: quelle di Dependency Injection, dove si propone come alternativa snella a librerie come Spring.
Esempio: controllo di sensori
Per osservare l'utilizzo della Dependency Injection ci serviamo di un esempio molto semplice. Immaginiamo di dover realizzare un sistema di controllo che periodicamente legga il valore di un sensore e provveda a utilizzare un servizio di logging per memorizzarlo. Il valore inoltre sarà confrontato con una soglia e se superiore a questa si dovrà inoltrare un allarme per segnalare l'evento.
Per favorire il riutilizzo di un sistema di questo tipo possiamo renderlo indipendente dal sistema di logging e dalla modalità di segnalazione dell'allarme ottenendo così un sistema flessibile in grado di adattarsi a diverse configurazioni. Come? Ricorrendo alle interfacce: nel codice allegato troverete Logging
e AlertDispatcher
. La nostra classe SensorGuard
coordina delle istanze di queste interfacce per portare a termine il suo compito:
public class SensorGuard {
private Logging logging;
private AlertDispatcher alertDispatcher;
private Timer timer = null;
@Inject
public SensorGuard(Logging logging, AlertDispatcher alertDispatcher) {
this.logging = logging;
this.alertDispatcher = alertDispatcher;
}
public void start(Sensor sensor, int threshold, int intervalInSeconds) {
if (this.timer != null) {
throw new IllegalStateException("Sensor guard already started!");
}
this.timer = new Timer();
this.timer.schedule(new GuardTask(sensor, threshold), intervalInSeconds*1000, intervalInSeconds*1000);
}
public void stop() {
if (this.timer == null) {
throw new IllegalStateException("Sensor guard not started!");
}
this.timer.cancel();
this.timer = null;
}
private class GuardTask extends TimerTask {
private int threshold;
private Sensor sensor;
public GuardTask(Sensor sensor, int threshold) {
this.sensor = sensor;
this.threshold = threshold;
}
public void run() {
int value = sensor.readValue();
if (value > threshold) {
logging.error("Sensor "+sensor.getName()+" returned the value "+value+"! It is above the threshold!");
alertDispatcher.sendAlert(sensor, value, threshold, new Date());
} else {
logging.info("Sensor "+sensor.getName()+" returned the value "+value);
}
}
}
}
Senza Google Guice
Nel codice in allegato ci sono anche le implementazioni delle interfacce di supporto utili per provare a testare la nostra classe SensorGuard
. Vediamo come utilizzarle per realizzare una piccola applicazione. Senza utilizzare Google Guice possiamo scrivere codice come questo:
public static void main(String[] args) {
SensorGuard sg = new SensorGuard(new ConsoleLogging(), new DummyAlertDispatcher());
sg.start(new DummySensor(), 88,1);
try {
Thread.sleep(10000);
System.out.println("STOP!!!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sg.stop();
}
}
Con Google Guice
Vediamo ora come potremmo gestire queste dipendenze con Google Guice. Per prima cosa dobbiamo scaricare la libreria dal sito del progetto. Al momento della stesura dell'articolo l'ultima versione disponibile è la 2.0.
Estraiamo i file dall'archivio ed inseriamo i jar aopalliance.jar
e guice-2.0.jar
(oppure guice-numero-versione.jar
) nel classpath.
Per segnalare a Google Guice che vogliamo utilizzare il suo sistema di Dependency Injection per istanziare una classe possiamo marcare un costruttore di quella classe con
l'annotazione @Inject.
A questo punto dobbiamo indicare a Guice quali classi specifiche andranno istanziate quando si imbatterà in ognuna delle interfacce che ci interessano. Queste informazioni le potremmo fornire tramite codice invece che tramite file di configurazione.
Per farlo basta creare una classe che estenda AbstractModule
e dichiarare il metodo void configure()
. Nel corpo di questo metodo specificheremo come risolvere le dipendenze inserendo per ogni coppia interfaccia-classe concreta da utilizzare una chiamata di questo tipo:
bind(MyInterface.class).to(MyConcreteClass.class);
Nel caso d'esempio otteniamo quindi questo codice:
public static class SensorGuardModule extends AbstractModule {
@Override
protected void configure() {
bind(Logging.class).to(ConsoleLogging.class);
bind(AlertDispatcher.class).to(DummyAlertDispatcher.class);
}
}
Ora non resta che utilizzare le informazioni su quali classi (e con quale costruttore) vadano istanziate tramite Dependency Injection e le informazioni relative a come gestire queste dipendenze (SensorGuardModule
) per istanziare concretamente un'istanza di SensorGuard
:
Injector injector = Guice.createInjector(new SensorGuardModule());
SensorGuard sg = injector.getInstance(SensorGuard.class);
Utilizzando Guice abbiamo ottenuto che la risoluzione delle dipendenze sia esplicitata in un punto preciso (SensorGuardModule
), invece che dispersa ad ogni utilizzo dell'operatore new
come era prima dell'introduzione di questa libreria.
Questo significa che possiamo riutilizzare quel modulo per la generazione di tutte le istanze di SensorGuard
che ci occorrano. Nel caso volessimo poi cambiare il sistema di dispaccio degli allarmi (scelta più che consigliata!) tutto quello che dovremmo fare è agire su SensorGuardModule
invece che su ogni dichiarazione presente nel codice.
Inoltre nel nostro esempio avevamo una sola classe, SensorGuard
, che aveva bisogno di gestire delle dipendenze; in un sistema reale è invece molto comune la situazione in cui molte classi hanno molte dipendenze, anche comuni, da interfacce di servizio.
Una nuova implementazione dell'interfaccia Dispatcher
Proviamo a modificare il nostro esempio per vedere meglio all'opera Guice. Proviamo a sostituire l'implementazione di AlertDispatcher
: invece di DummyAlertDispatcher
utilizziamo MoreSeriousAlertDispatcher
.
Mentre la precedente implementazione si limitava a scrivere un messaggio sulla console la nuova invia un'e-mail ad un certo indirizzo per comunicare l'allarme. Quando un'e-mail di allarme viene inviata si inserisce in un file di log una riga per indicare l'avvenuta spedizione.
Come si vede esaminando il codice allegato, in MoreSeriousAlertDispatcher
l'unico segno dell'utilizzo di Guice è l'annotazione @Inject
sul costruttore. Dobbiamo ora indicare in SensorGuardModule
che intendiamo utilizzare il nuovo sistema di dispaccio degli allarmi. Modifichiamo quindi la riga:
bind(AlertDispatcher.class).to(DummyAlertDispatcher.class);
in:
bind(AlertDispatcher.class).to(MoreSeriousAlertDispatcher.class);
Possiamo notare che anche MoreSeriousAlertDispatcher
, come SensorGuard
, necessita di un sistema di Logging
. La configurazione relativa è già presente in SensorGuardModule
per cui non dobbiamo fare altro. Guice si preoccuperà di generare la nostra istanza di SensorGuard
generando internamente un'istanza di ConsoleLogging
e di MoreSeriousAlertDispatcher
.
Per istanziare MoreSeriousAlertDispatcher
creerà inoltre un'altra istanza di ConsoleLogging
ma tutto questo senza bisogno del nostro intervento. Il nostro lavoro è finito specificando le dipendenze di SensorGuardModule
, lasciamo che sia il framework a gestire il ripetitivo lavoro di istanziazione al posto nostro.
Istanziazione di dipendenze su misura
Ci sono situazioni in cui abbiamo bisogno di maggiore controllo sull'istanziazione delle dipendenze. Uno dei motivi più comuni è la necessità di fornire parametri di configurazione necessari per creare l'istanza.
Ad esempio potremmo voler creare le istanze di MoreSeriousAlertDispatcher
fornendo al costruttore l'indirizzo e-mail a cui inviare il messaggio di errore. In una applicazione reale ben fatta un'informazione come questa potrebbe essere ricavata attraverso le fonti più svariate ma nel nostro caso sarà per semplicità una costante inserita nel codice.
Concretamente bisogna rimuovere la chiamata a bind
per AlertDispatcher
e sostituirla con un metodo da creare sempre all'interno di SensorGuardModule
. Questo metodo va marcato con l'annotazione @Provides
e Google Guice capisce a quale classe associarlo semplicemente guardando il tipo restituito. Il nome invece può essere quello che preferiamo:
@Provides
AlertDispatcher provideMyConfigurableMoreSeriousAlertDispatcher(Logging logging) {
return new ConfigurableMoreSeriousAlertDispatcher(logging, "myemail@html.it");
}
Dopo questa modifica ogni volta che viene incontrata una dipendenza da AlertDispatcher
la si risolve associandola al risultato dell'esecuzione di provideMyConfigurableMoreSeriousAlertDispatcher
. Potete notare che a sua volta provideMyConfigurableMoreSeriousAlertDispatcher
ha una dipendenza da Logging che viene gestita da Guice
.
Ricapitoliamo cosa succede quando invochiamo injector.getInstance(SensorGuard.class)
. Innanzitutto Guice si accorge osservando il costruttore di SensorGuard
che questa classe dipende da Logging
e AlertDispatcher
. Valuta quindi come soddisfarle: per la dipendenza da Logging
di SensorGuard
bisogna creare un'istanza di ConsoleLogging
(come indicato dal bind
nel metodo configure
di SensorGuardModule
). ConsoleLogging
non ha dipendenze per cui viene istanziata senza ulteriori passaggi.
Per la dipendenza da AlertDispatcher
di SensorGuard
bisogna invocare il metodo provideMyConfigurableMoreSeriousAlertDispatcher
. Il metodo in questione però a sua dipende da Logging
per cui viene creata una ulteriore istanza di ConsoleLogging
. Il metodo provideMyConfigurableMoreSeriousAlertDispatcher
viene finalmente eseguito. Avendo ottenuto le due istanze necessarie per eseguire il costruttore di SensorGuard
questo viene invocato ed il risultato ritornato dalla chiamata injector.getInstance(SensorGuard.class)
.
Già in uno scenario estremamente semplice come questo da noi esaminato, possiamo vedere che cosa Guice ci eviti di dover gestire manualmente.
Guice: solo l'inizio...
In questo articolo abbiamo esaminato la principale funzione di Google Guice ma non abbiamo certo esaurito tutte le possibilità che questa ha da offrirci. Di certo vale la pena approfondire ancora questo progetto che, oltre essere targato Google, ha ricevuto il riconoscimento prestigioso della 18esima edizione dei Jolt Award.