Articoli marcati con tag ‘framework’

Case study: implementazione event framework JSR-299 su OSGi

venerdì, 24 luglio 2009

Presso una software house, nostro cliente, siamo coinvolti nello sviluppo di un software di grosse dimensioni entro il quale abbiamo dato una interessante implementazione di un innovativo dispatcher di eventi che vogliamo illustrarvi.

Il software in fase di sviluppo è un front-end lato client per un’applicazione client-server di ambito enterprise. Essendo l’esigenza quella di costruire una piattaforma altamente modulare e configurabile a diverse granularità, è stato scelto di sviluppare il tutto attorno ad un kernel modulare basato su OSGi. La stessa tecnologia alla base di Lotus Notes 8, Eclipse e Websphere 7.

In questo progetto siamo coinvolti nello sviluppo di componenti infrastrutturale del framework al di sopra del quale i programmatori svilupperanno alcune centinaia di plugin per la gestione della UI e della logica applicativa, che potranno essere diversi o customizzati per ogni singola installazione. Uno degli obiettivi è di creare un modello di sviluppo semplice, veloce e sicuro che semplifichi la programmazione al top di OSGi.

L’esigenza emersa durante lo sviluppo era la carenza di un efficiente meccanismo di gestione degli eventi cross e intra plugin di facile uso. In questo post vorrei illustrare per sommi capi il nuovo meccanismo introdotto, in quante abbiamo usato tecniche particolarmente innovative e interessanti.

L’idea che abbiamo avuto è di dare una implementazione in OSGI, tramite tecniche AOP, delle parte eventing della nuova specifica Java denominata JSR-299 (attualmente in final draft).

JSR-299 è un superset delle tecnologie alla base di noti framework IoC come Spring e Google Guice, e ne estende le capacità introducendo un nuovo modello di programmazione contestuale (tornerò sull’argomento in un prossimo numero di Snapshot). Per quel che ci riguarda ci interessava la parte della specifica che definisce un modello di gestione degli eventi di tipo publish/subscribe, totalmente type-safe e totalmente disaccoppiato.

L’approccio classico, in Java, per questo tipo di problematiche è il pattern observer che prevede che un oggetto osservato implementi una qualche interfaccia tipo IObserver e che il notificatore fornisca un meccanismo di registrazione/deregistrazione alla fornitura di eventi. Di per sè il pattern è facile, ma dal punto di vista del programmatore finale, in un ambito OSGi, richiede uno sforzo non indifferente. L’implementazione degli observer è banale, ma la problematica sta nel fatto che l’osservatore deve conoscere la presenza dell’osservato e in un ambito altamente modulare questo non è possibile in modo triviale, se non introducendo meccanismi basati su registry centralizzati di eventi (come fa ad esempio Eclipse).

Il nuovo meccanismo introdotto da JSR-299 confida invece su un modello totalmente dichiarativo e bi-disaccoppiato reso type-safe dall’uso di annotation liberamente definibile dall’utente.

Facciamo un esempio

Supponiamo che un qualche oggetto, ad esempio un gestore di qualche widget grafico, voglia notificare un qualche evento grafico in modo agnostico rispetto agli osservatori interessati e ignorando meccanismi di registry. In JSR-299 l’approccio è il seguente:

public class UIMediator {
   private @UI @Update @BankAccount Event<AccountEvent> clickEvent;

   // ...
   public void onClick() {
      AccountEvent evt = new NewAccountEvent(name, surname);
      clickEvent.fire(evt);
   }

   // ...
}

il campo clickEvent è un astrazione prevista da JSR-299 di una coda di eventi tipizzata, in questo esempio sul generics AccountEvent. Il programmatore è libero di introdurre i propri tipi di evento anche in una gerarchia di tipo (si veda il NewAccountEvent dell’esempio). Le annotation @UI, @Update, @BankAccount sono anch’esse definite liberamente dal programmatore (a loro volta vanno marcate con una meta-annotation @BindingType) e definiscono una caratterizzazione “semantica” della coda di messaggi, utile per un’eventuale discriminazione da parte degli observer.

Nell’esempio il campo clickEvent è autoesplicativo: è una coda di messaggi di eventi relativi ad un nuovo account, proveniente dalla UI, relativi ad un update e relativi ad un account bancario.

L’inoltro di un evento si esegue invocato il campo fire che deve essere di tipo AccountEvent o un sottotipo. La firma è definita da un generics, per cui determinata staticamente a tempo di compilazione (o a tempo di digitazione in qualsiasi IDE moderno). Ciò elimina qualunque possibilità di errore di tipo da parte del programmatore finale.

E’ gli observer? Vediamo come può essere implementato un oggetto che venga notificato dell’evento. E’ sufficiente creare una annotato su un parametro.

public class BankAccountManager {

   // ...
   public void onNewAccount(@Observes @Update @BankAccount NewAccountEvent event) {
      //...
   }
}

In questo caso una qualunque istanza di questo oggetto viene notificata a fronte di un evento NewAccountEvent annotato almeno con annotation @Update e @BankAccount (è il caso dell’evento prima definito). Faccio notare come in questo caso l’osservatore non sia interessato a specificare la “semantica” @UI in quanto non è interessato alla fonte “fisica” dell’evento. Inoltre faccio notare come il tipo in ingresso sia NewAccountEvent sottotipo di AccountEvent (tipo della coda predente).

Tutto ciò matcha con la tipologia della coda del primo esempio per cui i due componenti “si parlano”.

E’ la sottoscrizione? Semplicemente non si fa. Qui subentra il tocco di magia dato dall’AOP che descrivo più sotto.

E’ sufficiente che in qualsivoglia plugin vengano istanziati gli oggetti affinchè la coda sia sia in grado di invocare tutti i metodi appena citati. Inoltre non è necessario istanziare il campo della coda in quanto esso è automaticamente iniettato dall’AOP alla prima lettura.

Come funziona

Tutto il dispatching degli eventi è gestito da un manager globale (singleton) residente in un plugin OSGi, non accessibile da altri plugin. Tale manager risolve tutte le problematiche di gerarchia di tipo e annotation semantiche e sa determinare chi è interessato a cosa.

Parallelamente vi sono degli aspect definiti via AspectJ (attivati tramite la libreria Eclipse Aspects per l’uso in OSGi) che intercettano la creazione di ogni oggetto contententi metodi del tipo citato. La sua istanziazione determina quindi automaticamente una registrazione presso il dispatcher globale. Un altro aspect controlla l’iniezione di campi Event<?>.

Per chi non fosse pratico di AOP, a tempo di runtime la cosa funziona così: ogni qualvolta una classe viene caricata dalla JVM il bytecode è analizzato e modificato nei punti definiti dichiarativamente dagli aspetto. Sostanzialmente, nel bytecode, ogni new di quelle tipologie di oggetti viene fatta seguire da una registrazione al dispatcher, esattamente come se il programmatore l’avesse scritto di proprio pugno. L’effetto è che non si ha alcun overhead di reflection eccetto quello di instrumentazione del bytecode (che comunque è cachato dopo la prima esecuzione).

Peculiarità implementative

Nel modello OSGi, i plugin possono aggiungersi e rimuoversi a caldo dall’applicazione e ciò introduce sia potenza che complessità nell’architettura. Nella nostra implementazione non vi è alcun aumento di complessità per il programmatore in quanto il dispatcher globale è in grado di rilevare ogni variazione di stato dei plugins e deregistrare e attivare automaticamente code e observer collegati.

L’intera implementazione è performante in quanto implementa meccanismi di caching in tutti quei punti ove vi sia un qualche uso di reflection (ad esempio nella risoluzione della gerarchia di tipo degli eventi).

Particolare attenzione è stata posta nella gestione della memoria. Il modello di memoria di Java è non deterministico e basato sulla garbage collection. Tutte le strutture del dispatchare globale sono implementate tramite softreference di modo che non vi sia generazione di riferimenti forti che proibiscano al garbage collector di eliminare oggetti non più riferibili. API di deregistrazione sono state implementate (al di fuori di JSR-299 che non le prevede), per forzare la deregistrazione deterministica nei casi in cui il programmatore lo ritenga necessario.

Gli observer possono sottoscriversi in modalità asinconia (basta un paramentro su @Observes) e in tal caso le notifiche avvengono in modo parallelo ed efficiente tramite un efficiente threadpool.

Conclusioni

Il framework risulta molto elegante e di semplicissimo uso da parte del programmatore. Inoltre induce la possibilità (e alla lunga anche l’obbligo) di introdurre una gerarchia di tipo degli eventi, annotazioni semantiche che chiariscono la leggibilità del codice, oltre ad un certo livello di isolamento. Infatti tipi di evento o di annotation definiti a livello privato di plugin, fanno sì che gli eventi rimangano privati in quanto altri plugin non hanno nozione del tipo di evento dispatchato e non vi si possono sottoscrivere.

Attualmente esso è in uso sia nella gestione di eventi cross-plugin, che nell’implementazione di pattern MVC a livello di plugin, che nella gestione di comunicazioni non grafiche interne da parte di servizi dati.

Infine, Google alla mano, crediamo che questa sia la prima implementazione data in ambito OSGi e tramite AOP.