Introduzione a Liferay Message Bus
Esistono in giro soluzioni Open Source di ogni tipo che implementano Enterprise Service Bus (ESB), Liferay ha preferito non integrare al suo interno una delle soluzioni esistenti (il più delle volte pensanti) in favore di un proprio "piccolo message bus" che fosse in grado di far comunicare in modo semplificato i componenti del portale tramite lo scambio di messaggi. La versione 6.1 di Liferay ha migliorato di molto il Message Bus introducendo un più ricco insieme di service API che facilitano lo scambio dei messaggi tra le portlet e in generale tra i plugins.
1. Utilizzo del Message Bus
I messaggi scambiati all'interno di Liferay tramite il Message Bus, sono esclusivamente di tipo String (in genere il formato adottato è JSON), così facendo l'accoppiamento tra producers e consumers è ancora più ridotto e inoltre sono evitati i problemi di caricamento delle classi. Il Message Bus è situato all'interno del kernel di Liferay, in questo modo sarà possibile accedere ai servizi del Bus da ogni applicazione installata. L'implementazione attuale non consente di inviare messaggi remoti che comunque possono essere inviati attraverso il cluster utilizzando le classi ClusterLink.
A qualcuno potrebbero venire in mente delle somiglianze con Java Message Service (JMS), il Message Bus di Liferay è molto più semplice e non fornisce tutte le caratteristiche della specifica JMS. Quanto mostrato in Figura 1 è una rappresentazione degli usi comuni del Message Bus.
Un esempio tangibile d'uso del Message Bus è appunto la portlet Message Board, questa, invia il messaggio sul bus restituendo la risposta all'utente, che può continuare a interagire con il portale. Chi riceve il messaggio dal bus (il listener), esegue una serie di azioni sul messaggio (nel caso di sottoscrizioni l'azione corrisponde all'invio di un'email a tutti i sottoscritti al messaggio) utilizzando un thread separato.
2. Architettura del Message Bus
Sono quattro i "pezzi" fondamentali che costituisco il Message Bus di Liferay e sono:
- Message Bus: Gestisce il canale di trasporto dei messaggi garantendo la consegna degli stessi dai senders ai listeners;
- Destinations: Sono gli indirizzi o destinazioni alle quali i listeners sono registrati affinchè possano ricevere i messaggi;
- Listeners: Sono i "consumatori" dei messaggi ricevuti nelle destinazioni. Questi ricevono tutti i messaggi inviati alle loro destinazioni registrate;
- Senders: Chi invia i messaggi sul bus verso una destinazione registrata.
E' possibile per cui inviare dei messaggi a una o più destinazioni e inoltre possibile configurare listener in ascolto su una o più destinazioni. Tanto per fare un esempio potremmo avere una situazione di questo tipo:
- Un client invia un messaggio a una destinazione chiamata shirusLabs/HorseGallops;
- Il Message Bus esegue un'interazione con ogni listener registrato sulla destinazione shirusLabs/HorseGallops;
- Per ogni listener è chiamato il metodo receive(String message).
Lo schema illustrato in Figura 2 è un possibile scenario d'utilizzo del Message Bus che chiarirà ancor di più le idee sull'architettura. Da notare come un servizio può inviare messaggi a una o più destinazioni e come un listener può essere in ascolto su più destinazioni, oltre al fatto che un servizio può essere sia sender sia listener.
Anche il Message Bus di Liferay come il resto delle altre soluzioni supporta principalmente due tipologie di messaggio:
- Synchronous messaging: Dopo l'invio di un messaggio, il mittente rimane in attesa di una risposta da parte del destinatario;
- Asynchronous messaging: Dopo l'invio di un messaggio, il mittente è libero di continuare l'elaborazione. Il mittente può essere configurato per ricevere una call-back o può semplicemente "inviare e dimenticare (Send-and-Forget)".
- Call-Back: Il mittente può essere configurato in modo tale che sia richiamato dal destinatario.
- Send-and-Forget: Il mittente non include nel messaggio informazioni di call-back e continua semplicemente con l'elaborazione.
Tutti gli elementi del Message Bus (destinations, listeners e mapping tra loro) sono magicamente configurabili tramite Spring. La configurazione del Message Bus è realizzata attraverso i seguenti file XML:
- WEB-INF/src/META-INF/messaging-spring.xml: All'interno di questo file devono essere definite le destinazioni, listeners e il mapping tra di loro;
- WEB-INF/web.xml : Contiene la lista dei deployment descriptors del plugin, in più occorre aggiungere alla lista anche il file di configurazione messaging-spring.xml di Spring.
Ogni listener definito in configurazione (fare riferimento a WEB-INF/src/META-INF/messaging-spring.xml) deve implementare l'interfaccia base MessageListener (com.liferay.portal.kernel.messaging.MessageListener) sovrascrivendo il metodo (vedi Figura 3):
- public void receive(Message message) throws MessageListenerException
Liferay fornisce una "facility" class astratta BaseMessageListener che implementa il metodo receive() dell'interfaccia MessageListener fornendo un metodo astratto doReceive(), il vostro listener può quindi estendere direttamente la classe BaseMessageListener implementando il metodo doReceive().
Nelle figure a seguire sono mostrati i sequence diagram delle due tipologie di messaggi: sincroni e asincroni.
Dalla Figura 5 e Figura 6 è possibile notare che ci sono due tipi di destinations per messaggi di tipo asincrono:
- Parallel Destination: Utilizzato nei casi in cui i messaggi devono essere inviati in parallelo. Su questo tipo di destinazione è possibile agire sulla configurazione del Thread Pool (Asynchronous “Send and Forget” vedi Figura 6);
- Serial Destination: Utilizzato nel caso in cui le richieste devono essere inviate in serie. In questo caso la dimensione del Thread Pool è impostata a uno (Asynchronous con callbacks vedi Figura 5).
In Figura 7 è invece indicato il class diagram che mostra le relazioni tra i tipi di destinazione implementati dal Message Bus di Liferay.
3. Configurazione
Nel precedente paragrafo abbiamo visto quali sono gli elementi che ruotano nell'intorno del Message Bus e qual è il file di configurazione tramite il quale è possibile configurare ogni singolo elemento. Potremmo suddividere la configurazione per sezioni, quali:
- Listeners: in questa sezione devono essere specificate una o più classi che gestiscono i messaggi che transitano sul Message Bus verso una o più destinazioni;
- Destinations: in questa sezione devono essere specificate una o più destinazioni dichiarando il tipo e il nome;
- Configurator: in questa sezione devono essere specificate le relazioni destinazione => listener.
A titolo esemplificativo sono mostrati a seguire le tre sezioni di configurazione di ogni elemento del Message Bus. Come potete notare la configurazione è in puro stile Spring.
<!– Listeners –> <bean id="messageListener.marketing_listener" class="it.dontesta.crm.messaging.impl.MarketingMessagingImpl" /> <bean id="messageListener.sales_listener" class="it.dontesta.crm.messaging.impl.SalesMessagingImpl" /> <bean id="messageListener.customer_support_listener" class="it.dontesta.crm.messaging.impl.CustomerSupportMessagingImpl" />
Source 1. Configurazione listener
La configurazione indicata al Source 1 dichiara tre diversi listener ognuno dei quali è una classe che implementa l'interfaccia MessageListener o estende la classe astratta BaseMessageListener.
<!-- Destinations --> <bean id="destination.crm.customer.ticket" class="com.liferay.portal.kernel.messaging.SynchronousDestination"> <property name="name" value="crm/customer/support/ticket"/> </bean> <bean id="destination.crm.customer.ticket.response" class="com.liferay.portal.kernel.messaging.SynchronousDestination"> <property name="name" value="crm/customer/support/ticket/response"/> </bean> <bean id="destination.crm.marketing.lead" class="com.liferay.portal.kernel.messaging.SerialDestination"> <property name="name" value="crm/marketing/lead"/> </bean> <bean id="destination.crm.marketing.lead" class="com.liferay.portal.kernel.messaging.SynchronousDestination"> <property name="name" value="crm/marketing/lead/response"/> </bean> <bean id="destination.crm.marketing.stream" class="com.liferay.portal.kernel.messaging.ParallelDestination"> <property name="name" value="crm/marketing/stream"/> </bean>
Source 2. Configurazione destinazioni
La configurazione indicata al Source 2 dichiara le destinazioni specificandone il tipo e il nome della destinazione. Il tipo indica la classe che implementa la Destination (vedi class diagram di Figura 7). Le destination supportate sono implementate dalle seguenti classi:
- com.liferay.portal.kernel.messaging.SynchronousDestination: per messaggio di tipo sincrono;
- com.liferay.portal.kernel.messaging.SerialDestination: per messaggio di tipo asincrono con call-back;
- com.liferay.portal.kernel.messaging.ParallelDestination: per messaggio di tipo asincrono send-and-forget.
Il nome della destinazione (vedi Source 2 riga 4) identifica questa all'interno del Message Bus e deve essere univoco; il nome assegnato deve essere a conosciuto dal consumer del servizio.
<!-- Configurator --> <bean id="messagingConfigurator" class="com.liferay.portal.kernel.messaging.config.PluginMessagingConfigurator"> <property name="messageListeners"> <map key-type="java.lang.String" value-type="java.util.List"> <entry key="crm/customer/support/ticket"> <list value-type="com.liferay.portal.kernel.messaging.MessageListener"> <ref bean="messageListener.customer_support_listener"/> </list> </entry> <entry key="crm/customer/support/ticket/response"> <list value-type="com.liferay.portal.kernel.messaging.MessageListener"> <ref bean="messageListener.customer_support_listener"/> </list> </entry> <entry key="crm/marketing/lead"> <list value-type="com.liferay.portal.kernel.messaging.MessageListener"> <ref bean="messageListener.marketing_listener"/> </list> </entry> </map> </property> <property name="destinations"> <list> <ref bean="destination.crm.customer.ticket"/> <ref bean="destination.crm.customer.ticket.response"/> <ref bean="destination.crm.marketing.lead"/> <ref bean="destination.crm.marketing.lead.response"/> </list> </property> </bean>
Source 3. Configurazione mapping destinazioni => listener
La configurazione indicata al Source 3 specifica le relazioni tra destinazione e listener. Le relazioni possibili sono quelle indicate in Figura 2. La configurazione core del Message Bus di Liferay è disponibile sul file messaging-core-spring.xml invece il file messaging-misc-spring.xml contiene le configurazioni di destination e listener dei componenti di "serie" di Liferay, alcune delle destinazioni sono:
- liferay/subscription_sender
- liferay/message_boards_mailing_list
- liferay/document_library_pdf_processor
Nel prossimo paragrafo vedremo un semplice esempio di come spedire delle notifiche via email inviando un messaggio verso la destinazione liferay/subscription_sender.
4. Invio messaggi sul Message Bus
Inviare un messaggio sul Message Bus e verso una specifica destinazione è molto semplice tramite il set di API a disposizione. Supponiamo il caso di voler inviare una o più notifiche via mail da una delle nostre portlet, per questo dovremmo preparare un messaggio con il contenuto della notifica e destinatari, in seguito inviare il messaggio verso la destinazione liferay/subscription_sender. In questo caso la destinazione è di tipo ParallelDestination (fare riferimento alla configurazione definita in messaging-misc-spring.xml), stiamo parlando quindi di un messaggio Asincrono (Send-and-Forget).
L'estratto di codice mostrato al Source 4 in breve:
- Prepara l'oggetto di tipo SubscriptionSender contenente il testo dell'email e destinatari;
- Crea il messaggio da inviare sul Message Bus specificando la destinazione e il contenuto informativo, quest'ultimo rappresentato dall'oggetto subscriptionSender (Serializable);
- Invia il messaggio appena creato sul Message Bus, questo consegnerà il messaggio alla destinazione indicata.
SubscriptionSender subscriptionSender = new SubscriptionSender(); subscriptionSender.setSubject("Test invio email via Message Bus"); subscriptionSender.setBody("Ecco la mail via Message Bus"); subscriptionSender.setUserId(user.getUserId()); ... subscriptionSender.addRuntimeSubscribers( user.getEmailAddress(), user.getFullName()); Message myMessage = new Message(); myMessage.setDestinationName("liferay/subscription_sender"); myMessage.setPayload(subscriptionSender); MessageBusUtil.sendMessage(myMessage.getDestinationName(), myMessage);
Source 4. Esempio d'invio email via Message Bus (MessageBusExample.java)
Il messaggio inviato sul Message Bus e destinato a liferay/subscription_sender è elaborato dal listener SubscriptionSenderMessageListener che effettivamente invia la mail. Sul mio repository GitHub l'esempio completo della portlet che interagisce con il Message Bus.
5. Conclusioni
Sono partito con l'intenzione di scrivere una serie d'articoli che trattano il Message Bus di Liferay. Con questo primo articolo della serie ho voluto introdurre le caratteristiche fondamentali di questo strumento evidenziando i principali vantaggi e in particolare la semplicità in termini d'uso e configurazione. Forse qualcuno potrebbe pensare che ho terminato con un semplice esempio, ho comunque in serbo per il prossimo articolo un caso di studio molto più interessante: Il Message Bus come mezzo d'integrazione verso altri sistemi.