La maggior parte degli sviluppatori Java ha acquisito familiarità con il concetto di Dependency Injection, un meccanismo di Inversione del Controllo (IoC), che consente a un componente di ottenere un riferimento a un altro componente tramite il container. JBoss Seam si avvale della IoC e anzi fa molto di più fornendo un meccanismo, denominato Bijection, che rende dinamico e bidirezionale il meccanismo di iniezione.
Introduzione
Il meccanismo della Dependency Injection(DI) è un meccanismo di Inversione del Controllo (IoC), in cui i componenti chiedono al container di farsi carico del compito di ottenere i riferimenti ai componenti con cui inizializzare lo stato di un componente. Questo pattern consente di avere un accoppiamento debole tra i componenti e consente alle applicazioni di focalizzarsi sul consumo dei servizi piuttosto che sul come localizzarli. JBoss Seam si avvale della IoC e fornisce un meccanismo di IoC dinamico e bidirezionale.
Uno dei principali limiti della dependency injection risiede infatti nella staticità del meccanismo, dal momento che l’injection viene eseguita quando il componente viene costruito, e i componenti iniettati non cambiano durante l’intero ciclo di vita dell’istanza creata. Per i componenti Stateless questo potrebbe non rappresentare un grosso limite e infatti viene spesso utilizzato durante la loro fase di istanziazione per stabilire i riferimenti verso i componenti dipendenti. Per quanto riguarda i componenti Stateful, il cui utilizzo è enfatizzato da JBoss Seam, la staticità della Dependent Inijection rappresenta un limite e è qui che il nuovo meccanismo, appunto la Bijection, fornisce molti vantaggi. Il punto chiave risiede nel fatto che la bijection è dinamica e quindi il legame con i componenti dipendenti non viene fatto unicamente durante la creazione dell’istanza, bensì ogni qual volta il componente viene invocato.
Bijection, una Dependency Injection potenziata
Con la DI, il container “inietta” gli oggetti dipendenti nei campi di un oggetto nel momento in cui quest’ultimo viene istanziato creando dei legami tra oggetti (un processo chiamato bean wiring). Questo meccanismo elimina dal codice le lookup degli oggetti e consente agli oggetti di avere degli accoppiamenti deboli con il resto dell’applicazione. Come detto in precedenza però, questo non basta, a causa delle limitazioni presenti. Prima di tutto le injections sono statiche, quindi applicate una volta sola quando l’oggetto viene istanziato. Fino a quando l’oggetto esiste continuerà ad avere questi riferimenti iniziali senza tener conto dei cambiamenti di stato che nel frattempo l’applicazione ha avuto.
Un’altra limitazione della DI è legata al fatto che è stata pensata solamente come metodo per assemblare un oggetto con i “pezzi” di cui è composto senza considerare il fatto che potrebbe essere utile anche invertire il processo e fare in modo che un oggetto possa trasferire il suo stato interno al container, tramite variabili di contesto. Seam ha dato risposta a queste domande introducendo la bijection. Con questo meccanismo, il legame con i componenti dipendenti viene fatto continuamente durante la vita di un componente (oltre che nel momento in cui viene istanziato). Inoltre, oltre alla injection è stata introdotta anche la outjection, ovvero il meccanismo che permette di trasferire lo stato di un componente (cioè i valori dei suoi campi) in variabili di contesto che potranno successivamente essere utilizzate da altri componenti, nelle view JSF, e così via.
Injection + Outjection = Bijection
Quindi la bijection può essere pensata come l’unione di questi due meccanismi, l’injection e la outjection, e viene gestita attraverso un method interceptor. La injection viene eseguita prima di ogni invocazione di metodo e si occupa di prendere le variabili di contesto necessarie dal container Seam e assegnarle ai campi dell’istanza del componente in questione. L’ outjection invece viene eseguita subito dopo che il metodo è stato completato e si occupa di trasferire i valori dei campi del componente nelle variabili di contesto.
Oltre a queste due fasi, ne esiste un’altra, la disinjection che ha il compito di resettare il componente mettendo il valore null in tutti i campi interessati dalla injection. Con questa fase, Seam rompe i legami tra i componenti quando questi non sono più necessari. Infatti, se una istanza di un componente viene resa idle, Seam non può aggiornarne lo stato a seguito di cambiamenti avvenuti nell’applicazione, diventando ben presto inconsistente. Per tale motivo Seam azzera i legami tra i componenti quando si completa l’outjection, legami che comunque verranno ricreati non appena il componente verrà invocato nuovamente.
Figura 1 – Method Call con la Bijection.
La bijection viene realizzata tramite un method interceptor, il bijection interceptor, che viene lanciato a ogni chiamata di metodo per effettuare un wrapping della chiamata con lo scopo di fornire la fase di injection prima, e quella di outjection e disinjection dopo l’effettiva esecuzione del metodo.
Annotazioni
I campi del componente che partecipano alla bijection vengono individuati tramite annotazioni, e tra queste le più comuni sono @In, per segnalare una proprietà per l’injection, e @Out, per l’outjection.
Quando un componente viene registrato, Seam effettua una scansione del componente alla ricerca di queste annotazioni e associa dei metadati al componente. Ogni volta che ci sarà una chiamata di metodo del componente, vengono esaminati i metadati e per ogni campo marcato con @In viene effettuata una ricerca del componente da iniettare. Successivamente verrà eseguito il metodo che potrà usare i valori di tali campi proprio come se fossero stati inizializzati esplicitamente. Se il metodo lancia un’eccezione, la bijection verrà interrotta e il controllo passerà al gestore dell’eccezione, altrimenti alla fine del metodo verrà effettuata una scansione dei campi annotati con @Out per esportare i loro valori nelle opportune variabili di contesto. Infine vengono resettati.
Tabella 1 – L’annotazione @In
Injection point
Per indicare a Seam di effettuare una injection viene utilizzata l’annotazione @In posizionata sopra il campo del componente o sopra il metodo setter. Seam userà la riflessione per effettuare l’assegnamento dei valori a tutti i campi annotati con @In nella prima fase della bijection.
L’attributo value può essere omesso, oppure contenere il nome di una variabile di contesto oppure ancora contenere un’espressione EL. Se viene omesso, verrà cercato un componente con nome coincidente col nome del campo, per cui tale attributo diventa fondamentale quando occorre usare un componente con nome diverso. Se si usa un’espressione EL, questa verrà prima valutata e poi si cercherà il componente con nome uguale al risultato ottenuto. Riportiamo un semplice esempio in cui viene usata l’annotazione @In e dove, subito dopo, all’interno di un metodo, viene usata la proprietà iniettata senza preoccuparsi della sua inizializzazione
@Name("registerAction") public class RegisterAction { @Logger private Log log; @In protected FacesMessages facesMessages; @In protected EntityManager entityManager; @In protected PasswordManager passwordManager; @In protected Cliente newClient; @In protected PasswordBean passwordBean; public String register() { ... entityManager.persist(newClient); facesMessages.add("Welcome to the site, #{newClient.name}!"); return "success"; } ... }
Ricerca della variabile di contesto
Per quanto riguarda la fase di ricerca della variabile di contesto, le operazioni effettuate sono le seguenti. Innanzitutto si controlla se l’annotazione @In contiene l’attributo scope indicante il contesto in cui andare a cercare. Se è presente si cercherà in quel contesto, se invece non è presente si effettuerà una ricerca gerarchica nei contesti stateful partendo dal contesto con vita più breve, EVENT, fino al contesto APPLICATION. La ricerca si ferma non appena si trova la prima variabile di contesto non nulla che soddisfa la chiave di ricerca. Se l’unica variabile trovata ha un valore null, e se è presente l’attribute create con valore true, Seam istanzia un componente, lo lega a una variabile di contesto e poi effettua l’injection. Se invece non si trova nessuna variabile di contesto (neanche con valore nullo), la proprietà rimarrà non inizializzata e eventualmente verrà lanciata un’eccezione RequiredException nel caso in cui è presente l’attributo required=true.
Figura 2 – Ricerca gerarchica all’interno dei contesti.
Altre annotazioni per l’injection
Oltre all’annotazione @In, Seam supporta altre annotazioni che rendono dinamico il processo dell’injection. Tra queste ricordiamo la @RequestParameter per effettuare l’injection di parametri di una richiesta HTTP nei campi di un componente, e la @PersistenceContext per effettuare l’injection di un EntityManager (JPA). Per quanto riguarda l’injection di parametri HTTP, essendo delle stringhe, devono essere associate a proprietà di tipo stringa, altrimenti verrà effettuata una conversione dei parametri attraverso il converter JSF registrato per quel particolare tipo.
Queste annotazioni potrebbero essere usate a esempio per creare una pagina che mostri il profilo di un cliente, profilo preparato dal componente profileAction. Se l’id del cliente viene passato tramite l’URL
http://localhost:8080/shop84/profile.seam?idCliente=1
possiamo usare l’annotazione @RequestParameter per iniettare l’idCliente in una proprietà del componente ogni qualvolta viene invocato uno dei suoi metodi. L’idCliente, che nel caso di @RequestParameter non sarà mai required, viene usato nel metodo load() per recuperare le informazioni attraverso l’EntityManager.
@Name("profileAction") public class ProfileAction { @PersistenceContext private EntityManager em; @RequestParameter protected Long idCliente; protected Cliente clienteSelezionato; public void load() { if (idCliente!= null && idCliente > 0) { clienteSelezionato = entityManager.find(Cliente.class, idCliente); } if (clienteSelezionato == null) { throw new ProfileNotFoundException(idCliente); } } }
Outjecting point
L’outjection, come abbiamo detto, serve a trasferire lo stato del componente nel container. Un outjection point viene dichiarato attraverso l’annotazione @Out posta sul campo o sul metodo getter. Dopo che il componente è stato invocato, i valori dei campi annotati sono usati per creare delle variabili di contesto o per settare il valore di variabili già esistenti. Se non è presente l’attributo value, verrà cercata una variabile di contesto con lo stesso nome della proprietà, altrimenti settando tale attributo è possibile associare la proprietà a qualsiasi variabile di contesto. A differenza dell’annotazione @In, @Out non supporta la notazione EL.
Tabella 2 – L’annotazione @Out
La fase di ricerca della variabile di contesto a cui legare la proprietà sarà effettuata tenendo conto della presenza o meno degli attributi value, scope e required. Per prima cosa viene ricavato il nome della variabile di contesto da cercare attraverso la presenza o meno dell’attributo value. Poi si valuta il valore della proprietà, si verifica la presenza dell’attributo required, e si valuta la compatibilità o meno del valore con quest’ultimo. Prima di passare alla fase di ricerca vera e propria viene controllata la presenza o meno dell’attributo scope. Se lo scope è indicato, Seam cerca la variabile di contesto nel contesto indicato (non viene accettato lo Stateless Context) altrimenti controllerà all’interno dei vari contesti. Se la ricerca ha buon esito e la variabile di contesto trovata ha un tipo uguale alla proprietà (o compatibile) allora si assegnerà il valore a tale variabile, altrimenti si lancerà un’eccezione.
Esempi di utilizzo dell’outjection
L’annotazione @Out è molto utile nel caso in cui sia necessario esporre il model alla view oppure quando si ha l’esigenza di passare dei dati a delle fasi di elaborazioni successive tramite l’inserimento di tali dati in scope con durata maggiore della durata del componente.
Vediamo qualche esempio. Un contesto si comporta esattamente come una mappa dove la chiave di ricerca è il nome della variabile di contesto. È possibile aggiungere una variabile a un contesto nel seguente modo:
@In protected Context eventContext; public void actionMethod() { eventContext.set("message", "Hello World!"); }
Usando l’outjection possiamo ottenere lo stesso risultato in maniera più lineare e in modo dichiarativo. Basta semplicemente annotare con @Out la proprietà da esportare e il lavoro verrà fatto da Seam.
@Out(scope = ScopeType.EVENT) protected String message; public void actionMethod() { message = "Hello World!"; }
In questo modo abbiamo disaccoppiato la variabile di contesto dalla logica di business. L’attributo scope dell’annotazione @Out è richiesto solo se lo scope target è differente dallo scope del componente che “ospita” la proprietà da esportare.
Completiamo ora il profilo del cliente iniziato precedentemente rendendo disponibile i suoi dati alla view /profile.xhtml. Per prelevare i dati del clienteSelezionato, prelevato dal metodo load() del componente ProfileAction, basta aggiungere @Out al campo clienteSelezionato. Il valore del campo è quindi assegnato a una variabile di contesto con lo stesso nome
@Name("profileAction") public class ProfileAction { @Out protected Cliente clienteSelezionato; ... public void load() { ... } }
La variabile di contesto, clienteSelezionato, può ora essere utilizzata all’interno della view /profile.xhtml nel seguente modo:
#{clienteSelezionato.nome}
Profile Titolo #{clienteSelezionato.titolo} Telefono #{clienteSelezionato.telefono} ...
Come abbiamo detto, l’outjection è utile anche per passare dei dati a elaborazioni successive. Con questo metodo possiamo dimenticare l’uso di campi nascosti e la propagazione manuale delle variabili da una request alla successiva. Basta semplicemente usare l’annotazione @Out su queste proprietà e usare @In nei componenti che le useranno successivamente. In questo modo riusciamo a disaccoppiare i vari componenti e la logica di business.
Conclusioni
Questo articolo conclude la serie dedicata all’analisi di alcune caratteristiche di JBoss Seam. Abbiamo trattato il meccanismo di IoC di Seam, la Bijection, di tipo dinamico e bidirezionale, e abbiamo anche visto l’uso delle relative annotazioni con dei semplici esempi di utilizzo. Le funzionalità di JBoss Seam non finiscono qua e coloro che intendano approfondirne lo studio e le caratteristiche possono trovare tutte le informazioni necessarie nella ricca e completa documentazione presente nel sito ufficiale del progetto [1] e nel libro di Dan Allen [2].
Riferimenti
[1] Sito di riferimento per il Framework Seam
[2] Dan Allen, “Seam in Action”, Manning Publications, 2009