Eccoci qua, di nuovo! Siamo stati un pò assenti ultimamente ma, buon per noi, abbiamo avuto parecchio da fare.
Oggi parliamo di OSGi e di un problema spinoso che tutti abbiamo incontrato almeno una volta: la dipendenza circolare ossia quella sfortunata casistica per cui un bundle A dipende da B e il bundle B dipende da A, cosa che ne impedisce il corretto funzionamento. A volte la dipendenza è diretta tra 2 bundle, altre volte invece riguarda più bundle ma sempre problematica è.
Spesso la soluzione alla dipendenza circolare è quella di realizzare un terzo bundle C che abbia entrambe le dipendenze A e B; purtroppo però non potevo seguire questa strada perché avrei dovuto cimentarmi in un refactor massiccio del codice e non ne avevo la possibilità (e ovviamente la voglia
).
Così ho cercato altre strade e ho scelto quella degli eventi OSGi. Ma cosa sono gli eventi OSGi? Sono un sistema, fornito dal container, per creare un canale di comunicazione tra i bundle mantenendoli però disaccoppiati in termini di dipendenze (come il Message Bus di Liferay per intenderci perché sì, stavo lavorando con Liferay 7.3).
Premetto però una cosa: gli eventi OSGi non sono stati creati specificatamente per risolvere il problema della dipendenza circolare, però in caso di necessità possono gestire il problema in modo abbastanza elegante.
L'idea alla base è molto semplice tutto sommato: il bundle A prepara l'evento con tutte le property che possono servire e invia l'evento sul bus OSGi. Il bundle B implementa un event handler che riceve l'evento, lo spacchetta e fa quello che deve fare, un pò come avviene in un qualunque sistema di gestione delle code.
L'invio dell'evento può avvenire sia in modalità asincrona che in modalità sincrona a seconda delle necessità: in entrambi i casi però non è previsto il ritorno di un risultato.
Vediamo quindi come fare per creare e inviare un evento OSGi dal bundle A.
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import java.util.Dictionary;
import java.util.Hashtable;
@Component(immediate = true, service = MiaClasse.class)
public class MiaClasse {
@Reference
private EventAdmin _eventAdmin;
public void mioMetodo(long value1, String value2, boolean value3) {
// Parametri da inviare all'event handler
Dictionary<String, Object> props = new Hashtable<>();
props.put("param1", value1);
props.put("param2", value2);
props.put("param3", value3);
// Creazione dell'evento OSGi
Event event = new Event("nome/del/topic/dell/evento", props);
// Invio asincrono dell'evento; il codice prosegue immediatamente
_eventAdmin.postEvent(event);
// Oppure in alternativa...
// Invio sincrono dell'evento; il codice resta in attesa finché l'event handler non ha terminato
_eventAdmin.sendEvent(event);
}
}
Come possiamo vedere il codice è piuttosto semplice: è sufficiente istanziare una mappa per i parametri, un oggetto Event e inviarlo. L'unica vera accortezza da tenere è il nome dell'evento (o topic in gergo OSGi) che deve seguire la sintassi indicata ossia niente spazi o punti ma stringhe separate da slash; è un pò come la destination di Liferay.
Ora vediamo invece il codice dell'event handler da implementare nel bundle B.
import com.liferay.portal.kernel.util.GetterUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
@Component(property = { "event.topics=nome/del/topic/dell/evento" }, service = EventHandler.class)
public class MioEventHandler implements EventHandler {
@Override
public void handleEvent(Event event) {
// Recupero dei parametri dell'evento
long value1 = GetterUtil.getLong(event.getProperty("param1"));
String value2 = GetterUtil.getString(event.getProperty("param2"));
boolean value3 = GetterUtil.getBoolean(event.getProperty("param3"));
// Esecuzione del codice applicativo
// Nessun valore di ritorno!
}
}
Anche in questo caso le cose sono piuttosto semplici:
- si implementa un componente che estende EventHandler
- si annota il componente con lo stesso topic usato nell'altra classe (altrimenti l'event handler non sa a cosa reagire)
- si recuperano i parametri dell'evento e si esegue il codice necessario
Tutto questo avviene in maniera totalmente disaccoppiata, senza che i bundle A e B si "conoscano" o abbiano dipendenze reciproche.
Bello ma se avessi davvero bisogno di un valore di ritorno? Il meccanismo degli eventi OSGi non lo prevede ma lavorando un pò con classi anonime ed espressioni lambda riusciamo di fatto a implementare un meccanismo di callback come se fosse Javascript; vediamo quindi come modificare il codice!
Innanzitutto supponiamo che il valore di ritorno che ci serve sia un booleano (chiaramente adeguate il codice alle vostre esigenze) e definiamo un'interfaccia che ce lo faccia gestire.
public interface BooleanResultHandler {
// Il parametro è il valore di ritorno
void onResult(boolean result);
}
Ora invece vediamo come modificare la classe che genera l'evento.
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.concurrent.atomic.AtomicBoolean
@Component(immediate = true, service = MiaClasse.class)
public class MiaClasse {
@Reference
private EventAdmin _eventAdmin;
public boolean mioMetodo(long value1, String value2, boolean value3) {
// Per gestire il valore di ritorno con coerenza durante tutto il flusso
// non basta una variabile final ma dobbiamo scomodare gli oggetti Atomic
AtomicBoolean returnValue = new AtomicBoolean(false);
// Sfruttando l'espressione lambda, definisco la classe anonima di callback
// che sarà usata dall'event handler per passare il valore di ritorno
BooleanResultHandler callback = result -> returnValue.set(result);
// Parametri da inviare all'event handler a cui aggiungo la callback
Dictionary<String, Object> props = new Hashtable<>();
props.put("param1", value1);
props.put("param2", value2);
props.put("param3", value3);
props.put("callback", callback); // Nuovo parametro dell'evento!
// Creazione dell'evento OSGi
Event event = new Event("nome/del/topic/dell/evento", props);
// Invio sincrono dell'evento; il codice resta in attesa finché l'event handler non ha terminato
// DEVE essere sincrono se vogliamo gestire il valore di ritorno!
_eventAdmin.sendEvent(event);
return resultValue.get();
}
}
Le modifiche al codice non sono poi così tante: si tratta di definire una classe di callback da inviare all'event handler in modo da salvare il valore di ritorno.
Infine vediamo cosa deve fare l'event handler per fornirci il valore di ritorno.
import com.liferay.portal.kernel.util.GetterUtil;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
@Component(property = { "event.topics=nome/del/topic/dell/evento" }, service = EventHandler.class)
public class MioEventHandler implements EventHandler {
@Override
public void handleEvent(Event event) {
// Recupero dei parametri dell'evento
long value1 = GetterUtil.getLong(event.getProperty("param1"));
String value2 = GetterUtil.getString(event.getProperty("param2"));
boolean value3 = GetterUtil.getBoolean(event.getProperty("param3"));
BooleanResultHandler callback = (BooleanResultHandler) event.getProperty("callback");
// Esecuzione del codice applicativo
// che terminerà con la valorizzazione del valore di ritorno
boolean result = ...
// Ecco finalmente il valore di ritorno inviato alla classe di callback e NON restituito dal metodo
callback.onResult(result);
}
}
Ecco quindi concluso tutto il giro!
La prima classe genera l'evento insieme alla callback e poi aspetta (perché l'invio è sincrono).
La seconda classe riceve l'evento, fa quello che deve fare e poi invoca il metodo della callback; questo fa sì che sulla prima classe venga salvato il valore di ritorno che può quindi essere usato.
E per oggi è tutto! Spero di avervi risolto un problema.
Enjoy!