vai al contenuto principale

Ciclo di sviluppo

Nell’Introduzione e in Ambiente di esecuzione abbiamo affermato che:

  • Un plugin è l’implementazione Java di una o piĂą interfacce definite nella SPI, generata a partire dai Service Handler dichiarati in un modello.
    Per brevità, d’ora in poi ci riferiremo ai Service Handler con il termine Handler.
  • Un plugin è un bundle che può essere installato ed eseguito su una Cloudlet la cui applicazione è generata a partire dallo stesso modello.
  • Plugin e Cloudlet interagiscono a runtime pubblicando e ricercando servizi all’interno del container OSGi.

In questo articolo illustreremo i passi che compongono il ciclo di sviluppo di un plugin:

  1. Modellazione degli Handler
  2. Configurazione del progetto di sviluppo
  3. Sviluppo del codice
  4. Build e deploy

Nel diagramma che segue abbiamo raffigurato gli ambienti e gli strumenti coinvolti:

Plugin lifecycle

Lo sviluppo inizia nel Designer, dove prepariamo un modello per l’utilizzo con gli Handler desiderati; si prosegue nel nostro IDE, dove configuriamo invece il progetto di sviluppo su cui andremo poi a implementare le Handler interface (contenute nel jar SPI della Cloudlet) insieme a tutto il codice di supporto del nostro plugin. Effettuando il build del progetto, otterremo in output il jar bundle OSGi che andremo a installare ed avviare sulla Cloudlet.

Al centro è raffigurato il container OSGi in cui vive la Cloudlet e che ospiterà il nostro Plugin dopo il deploy. L’interconnessione tra le interfacce è gestito automaticamente da OSGi dopo lo start del bundle. Le funzioni per gestire deploy, start, stop e delete dei Plugin della Cloudlet sono disponibili dalla Dashboard in un pannello apposito a cui possiamo accedere aprendo il menu in basso a destra () e cliccando su Manage plugins.

Dash cloudlet menu manage plugins

Dash cloudlet plugins manager sample

Modellazione degli Handler #

Il primo passo consiste nello scegliere quali Handler utilizzare. Come abbiamo accennato, ne esistono diversi tipi, ciascuno dei quali si distingue per il contesto in cui opera (inteso come “punto dell’applicazione in cui l’Handler consente di iniettare codice”). Ciascun Handler richiede una configurazione differente nel modello, che si rifletterà sulla SPI successivamente generata.

Possiamo distinguere due gruppi di Handler: quelli inseriti all’interno di una delle fasi del salvataggio dei dati (modifica, validazione, commit della transazione sul database) e quelli che vengono richiamati in risposta a sottomissioni di classi form; anche questi eventi possono essere modellati personalizzando l’Application Schema. Ci riferiamo ai due gruppi di Handler rispettivamente con i nomi Persistency Handlers e Interaction Handlers.

Handlers

Oltre ai due gruppi sopracitati (gli Handler modellabili), esiste un terzo gruppo denominato Handler di default; si tratta di Handler offerti di default dalla Cloudlet, le cui SPI vengono quindi sempre generate a prescindere dal modello. Tra questi rientrano gli Handler inseriti nelle fasi di login e logout dei membri della Cloudlet e gli Scheduled Task.

Per conoscere tutti i tipi di Handler e la loro configurazione nel modello rimandiamo al seguente articolo: Tipi di Handler e modellazione.

Download delle SPI #

Una volta avviata la Cloudlet con il modello configurato con gli Handler, Livebase genererà le interfacce SPI corrispondenti; queste saranno contenute nel jar SPI, che può essere scaricato dal menu Manage plugins cliccando sulla voce Download Cloudlet SPI.

Dash cloudlet plugins manager

Jar SPI e aggiornamento del modello #

Il jar SPI “lega” un modello ai suoi plugin. Pertanto, ogni volta che si modifica un qualche elemento di un modello con degli Handler e si rigenera l’applicazione, è molto probabile che venga rigenerata anche una nuova versione del jar SPI contenente le interfacce aggiornate; il disallineamento che ne consegue può portare al verificarsi di errori di compilazione e/o runtime per i plugin definiti sulla vecchia versione del jar SPI.

Per questo motivo, ogni volta che si rigenera un’applicazione con dei plugin, sarà necessario:

  • Scaricare la nuova versione del jar SPI;
  • Verificare che non siano state introdotte inconsistenze col codice sorgente e risolvere eventuali errori di compilazione;
  • Effettuare di nuovo build e installazione dei plugin.

Configurazione del progetto #

Il bundle completo di un plugin deve contenere i seguenti elementi:

  • (almeno un) Package contenente le Handler implementation compilate in file .class;
  • (eventuali) Librerie di supporto a runtime per le Handler implementation.
  • Un file manifest.mf correttamente configurato per esportare i package e le librerie sopracitate nel container OSGi.
  • Un blueprint.xml configurato per inizializzare i servizi nel container OSGi.

La buona notizia è che possiamo evitare di dover gestire manualmente gran parte della “burocrazia” dietro la configurazione del bundle: una buona pratica consiste infatti nel realizzare dei task Gradle che effettuino automaticamente la configurazione OSGi del nostro plugin. Andando oltre, possiamo avvalerci di Gradle per automatizzare il build e il packaging del nostro plugin, oltre che per gestirne automaticamente le dipendenze.
Tipicamente, un progetto per lo sviluppo di un plugin è quindi un progetto Gradle che richiede la scrittura di un build script. Non dovrai fare niente di tutto questo! Per semplificarti la vita, abbiamo infatti reso disponibili alcuni progetti base.

Sviluppo del codice #

Dopo aver scelto e configurato adeguatamente un progetto base, potrai subito creare delle classi ed implementare le SPI. Dando un’occhiata al jar SPI importato, possiamo vedere come esso contenga tutte le interface degli Handler che abbiamo definito nel modello; queste sono contenute nel package com.fhoster.livebase.cloudlet.

Il nome delle Handler Interface generate segue questo schema:

Spi{ClassName}{HandlerType}{HandlerName}

Ad esempio, a un FormActionHandler chiamato doStuff dichiarato sulla classe Person corrisponderà l’interfaccia SpiPersonFormActionHandlerDoStuff.

Classe coinvoltaTipoNome dell’HandlerInterfaccia da implementare
PersonInsertlogSpiPersonDatabaseInsertHandlerLog
PersonUpdatemyUpdateSpiPersonDatabaseInsertHandlerMyUpdate
PersonDeletemyDeleteSpiPersonDatabaseDeleteHandlerMyDelete
PersonFormActiondoStuffSpiPersonFormActionHandlerDoStuff

Con il content assist abilitato nel nostro IDE, possiamo digitare implements Spi e premere CTRL+spazio per farci suggerire tutte le SPI disponibili. La lista include anche le interfacce degli Handler di default.

Una volta scelta la SPI dovremo implementare i metodi da essa definiti. Ad esempio, un FormActionHandler definisce un metodo doAction(), i DatabaseHandler definiscono i metodi beforeCommit() e afterCommit(), uno ScheduledTask il metodo run().

Ciascun metodo può avere come parametro un oggetto context, che consente di accedere a elementi dell’applicazione coinvolti nell’esecuzione dell’Handler, come ad esempio:

  • connessioni aperte al database della Cloudlet. Si può accedere alla connessione correlata all’evento in atto attraverso l’invocazione del metodo getConnection(), ed utilizzarla per sottomettere delle query SQL personalizzate;
  • Bean/Entity: rappresentazione di un oggetto di una classe del modello. Tipicamente, gli handler permettono di accedere a due versioni della entity:
    • la versione precedente alla modifica in atto, mediante l’invocazione del metodo getOldEntity(), che sarĂ  accessibile in sola lettura;
    • la versione in corso di modifica, mediante l’invocazione del metodo getNewEntity(), che sarĂ  accessibile sia in lettura che in scrittura, se l’handler lo permette.
  • Member: rappresentazione dell’utente della Cloudlet coinvolto nell’evento, accessibile con get__CloudletCurrentUser();
  • la sessione corrente: consente di effettuare operazioni CRUD sulle classi del modello tra quelle accessibili dal membro che ha scatenato l’evento. In base all’Handler, la sessione può essere acceduta nella sua versione immutabile (chiamando getCloudletImmutableEntitySession()) o mutabile (getCloudletEntitySession()).

Ciascun metodo richiede di definire un risultato che determinerà quale azione sarà eseguita dalla Cloudlet al completamento dell’invocazione dell’Handler; questo risultato va costruito a partire dal context, invocando su di esso il metodo result() ed eseguendo in catena un’ulteriore serie di invocazioni in funzione dell’azione richiesta. Ad esempio, il metodo doAction() di un FormActionHandler può ritornare context.result().withMessage().info("hello") e far restituire il messaggio “hello” in un’apposita risposta GraphQL. Eventualmente possiamo decidere anche di non eseguire nessuna azione al termine dell’esecuzione, ritornando context.result().none().

Codice a supporto delle SPI #

Tutte le interfacce e le classi di supporto ai metodi definiti nelle SPI (tra cui i vari Context) sono anch’essi inclusi nel jar SPI, sotto il package com.fhoster.livebase.cloudlet. Consideriamo ad esempio un FormActionHandler definito su una classe Person: dal momento che il suo context permette di accedere all’oggetto – con il metodo get() – su cui l’Handler è invocato, nel package avremo a disposizione il bean/entity Person più tutti gli eventuali bean/entity delle classi associate a esso.

Oltre al context, durante l’esecuzione dell’Handler abbiamo a disposizione diversi servizi della Cloudlet. Attualmente sono disponibili i seguenti:

  • CloudletIdGenerator: permette di generare ID univoci per un record di tabella, essenziale in tutti gli scenari in cui vogliamo aggiungere nuovi oggetti sul database mediante plugin, in quanto garantisce che essi abbiano un ID univoco;
  • CloudletDataSource: permette di accedere al dataSource della Cloudlet ed eseguire direttamente query al suo database;
  • CloudletMailSystem: permette di inviare email via Plugin;
  • CloudletMemberManager: permette di gestire i membri della Cloudlet via Plugin;
  • CloudletSessionFactory: permette di ottenere una sessione attiva.

Per utilizzare un servizio è sufficiente dichiararlo come parametro da costruttore della classe che implementa l’Handler; l’applicazione eseguirà automaticamente la injection dell’istanza durante l’inizializzazione del nostro Plugin.

Il seguente è un esempio di utilizzo completo di Handler Implementation:

import java.math.BigInteger;
import org.apache.log4j.Logger;
import com.fhoster.livebase.CloudletScheduledTask;
import com.fhoster.livebase.cloudlet.CloudletIdGenerator;
import com.fhoster.livebase.cloudlet.SpiCloudletScheduledTask;

@CloudletScheduledTask(defaultCronExpression = "* * * * *")
public class MyScheduledTask implements SpiCloudletScheduledTask {
  private CloudletIdGenerator idGenerator;
  private static Logger logger = Logger.getLogger(MyScheduledTask.class);
  public MyScheduledTask(CloudletIdGenerator idGenerator) {
    this.idGenerator = idGenerator;
  }

  @Override
  public void run() {
    BigInteger id = idGenerator.get();
    String msg = "I generate pointless ids every minute. Current id: " + id;
    logger.info(msg);
  }
}

Annotazioni #

Le classi che contengono le implementazioni degli handler devono essere opportunamente annotate, affinché l’Annotation Processor incluso nel progetto base possa generare il blueprint del Plugin, con tutti i servizi della Cloudlet che abbiamo dichiarato come parametri del costruttore.

In particolare, queste classi devono essere decorate con l’annotazione @CloudletEventHandler, fatta eccezione per quelle che contengono implementazioni di ScheduledTask, che invece vanno annotate con @CloudletScheduledTask, e quelle che definiscono CloudletRestletPlugin, che richiedono l’annotazione @CloudletRestletServerResource

Per maggiori informazioni rimandiamo alla documentazione dell’Annotation Processor.

Librerie esterne #

Nell’esempio di sopra abbiamo inoltre utilizzato la libreria Log4J per il logging del Plugin all’interno dell’applicazione. Oltre al codice disponibile nel jar SPI, nei nostri plugin possiamo infatti utilizzare librerie Java esterne, configurando il manifest via Gradle.

In particolare, la libreria di logging è inclusa nel container OSGi, e pertanto può essere utilizzata allo stesso modo del jar SPI dichiarandola come compileOnly e senza doverla quindi includere nel bundle in output. Tutte le altre librerie esterne devono essere invece dichiarate come implementation.

Per saperne di piĂą #

Per ottenere una descrizione esaustiva dello SPI, puoi consultare la documentazione Javadoc specifica della tua Cloudlet (che include i bean/entity del modello), raggiungibile aggiungendo /docs/index.html all’indirizzo URL della Cloudlet.

Build del progetto e deploy #

Una volta implementato il codice del plugin, non resta altro che lanciare il comando gradle build. Gradle compilerà e testerà il codice, eseguirà l’Annotation Processor per generare i blueprint, collezionerà le dipendenze e genererà un bundle OSGi.

Il bundle si troverà in output/ nella root del progetto; il nome di default è projectName-spiVersion; possiamo cambiare questo valore editando jar.archiveName in build.gradle e il file gradle.properties.

Il jar risultante avrĂ  la seguente struttura:

archiveName/
├── packages/
│   └──.../
├── OSGI-INF/
│   └── blueprint/
├── META-INF/
│   └── manifest.mf
└── lib/
  • OSGI-INF contiene i blueprint generati dall’Annotation Processor.
  • META-INF contiene il file manifest.mf che dichiara le dipendenze del progetto.
  • lib contiene le dipendenze incluse nel progetto.

A questo punto possiamo aprire il pannello Manage plugins della nostra Cloudlet, cliccare su Upload... e selezionare il nostro Plugin; dopodiché clicchiamo su Refresh: se l’upload è andato a buon fine, il plugin entrerà nello stato Resolved. Cliccando sull’icona , il Plugin verrà avviato e le nostre classi verranno istanziate sul registro dei servizi OSGi. Cliccando di nuovo su Refresh, il plugin entrerà nello stato Active: da questo momento in poi, il nostro Plugin è in esecuzione sulla Cloudlet.

Possiamo verificarne il funzionamento accedendo all’applicazione generata e scatenando l’evento che abbiamo programmato. Inoltre, possiamo consultare i log della Cloudlet per verificare, ad esempio, l’esecuzione degli ScheduledTask.