MDB(Message Driven Bean)

MDB - Un introduzione su Message Driven Bean attraverso un Esempio
G.Morreale

Introduzione:

Un MDB(Message Driven Bean) è un particolare tipo di bean in grado di eseguire determinate operazioni in relazione alla ricezione di un messaggio JMS.
Esso quindi non risponde alle richieste di un client ma agisce il relazione all'evento "ricezione di un messaggio".

La ricezione a secondo del dominio scelto avviene o su un topic o su una coda.

L'MDB non è altro che un client JMS che opera in fase di ricezione su una coda.

L'Esempio

Supponiamo di voler realizzare un MDB in grado di elaborare le richieste web che arrivano su un determinato url(una servlet).
L'MDB collezionerà i dati presenti nell'oggetto HttpServletRequest al fine di conservarli su un file di log.

Quindi ad ogni accesso l'MDB apre il file, e scrive all'interno delle informazioni: ip del client, user-agent, timestamp della richiesta etc.

Ma perchè far eseguire tali operazioni all'MDB? L'asincronicità del sistema JMS consente di non bloccare l'esecuzione della servlet in attesa delle operazioni(apertura, scrittura file etc.) di log(inutili per l'utente).

Predisporre i progetti

Al fine di realizzare il progetto creiamo un applicazione EAR dotata di modulo WEB e modulo EJB.
Nel modulo WEB inseriamo il produttore di messaggi nell'EJB inseriremo il consumatore, ovvero l'MDB.

Elementi dell'esempio

Come in ogni applicazione JMS si avrà 3 elementi fondamentali:

  • provider dei messaggi 
  • client che produce i messaggi
  • client che consuma i messaggi

Ma commentiamo l'elenco rapportandolo all'esempio

  • provider dei messaggi - Coda da creare tra le risorse dell'application server in grado di raccogliere i messaggi prodotti.
  • client che produce i messaggi - Servlet (o meglio un suo filtro) che invia il messaggio di accesso all'url
  • client che consuma i messaggi - MDB che elabora il messaggio memorizzando alcuni dati su file.

Configurazione del Provider JMS

Premetto che il provider, o meglio la sua configurazione, è legata all'application server utilizzato; in questo caso viene usato Glassfish V2.

Apriamo il pannello di amministrazione. Ad esempio su localhost:4848.
Su Resources -> JMS Resources -> Connection Factories 
è possibile creare una nuova connection factory:



inseriamo il JNDI name, ovvero il valore che consente di ritrovare all'interno del container la risorsa, impostiamo come tipologia di risorsa una QueueConnectionFactory in quanto vogliamo utilizzare nello specifico una coda e lasciamo invariate tutte le altre opzioni.

nota:
Glassfish crea un pool di connessioni verso la coda in modo da ottimizzare il riuso di connessioni aperte verso la factory allo stesso modo in cui ottimizza ad esempio quelle jdbc verso un db.

Allo stesso modo è possibile creare una Destination:

Scegliendo come al solito il nome JNDI, il nome fisico della destination e la tipologia di destination che in questo caso è una coda.

Client che produce i messaggi

Il messaggio verrà prodotto in relazione all'accesso a un determinato url, useremo 

http://localhost/mdb/test

Quindi dovremo creare la servlet test e un filtro per la servlet test, ovviamente nel progetto web(war).

nota:
I filtri intecettano le request e response di un determinato url pattern al fine di modificarli e ripassarli alla servlet o jsp chiamata o adirittura ad uno o più filtri. Forniscono la possibilità di creare unità di codice riusabile su più url (servlet o jsp), nonchè modularizzare il codice e creare delle vere e proprie catene di filtri(strutturando lo sviluppo in una sorta di plugins)

Riguardo la servlet realizziamo una banalissima hello world servlet:

package servlet;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 *
 * @author PeppeM
 */
public class test2 extends HttpServlet {
   
    /** 
    * Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
    * @param request servlet request
    * @param response servlet response
    */
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {            
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet test2</title>");  
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>ESEMPIO MDB - WEB TRACKING</h1>");
            out.println("</body>");
            out.println("</html>");            
        } finally { 
            out.close();
        }
    } 

    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
    /** 
    * Handles the HTTP <code>GET</code> method.
    * @param request servlet request
    * @param response servlet response
    */
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        processRequest(request, response);
    } 

    /** 
    * Handles the HTTP <code>POST</code> method.
    * @param request servlet request
    * @param response servlet response
    */
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
        processRequest(request, response);
    }

    /** 
    * Returns a short description of the servlet.
    */
    public String getServletInfo() {
        return "Short description";
    }// </editor-fold>

}

Predisponiamo un oggetto DTO(Data Transfer Object)

In grado di incapsulare i dati che serviranno al client consumatore!
Tale oggetto è opportuno inserirlo nel progetto EJB e non in quello WEB.


import java.io.Serializable;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;


public class RequestDTO implements Serializable{
    String user_agent;
    private String remote_addr;
    private String query_string;
    private String method;
    private Date date;

    public String getMethod()
    {
        return method;
    }

    public void setMethod(String method)
    {
        this.method = method;
    }

    public String getQuery_string()
    {
        return query_string;
    }

    public void setQuery_string(String query_string)
    {
        this.query_string = query_string;
    }

    public String getRemote_addr()
    {
        return remote_addr;
    }

    public void setRemote_addr(String remote_addr)
    {
        this.remote_addr = remote_addr;
    }

    public String getUser_agent()
    {
        return user_agent;
    }

    public void setUser_agent(String user_agent)
    {
        this.user_agent = user_agent;
    }
   
    public RequestDTO(HttpServletRequest request)
    {
        user_agent = request.getHeader("user-agent");
        remote_addr = request.getRemoteAddr();
        query_string = request.getQueryString();
        method = request.getMethod();
        date = new Date();
    }

    @Override
    public String toString()
    {
        return date + " " + this.remote_addr + " " +  this.getMethod() + " " +  this.getUser_agent() + " " +  this.query_string + "\r\n";
    }
}


E' un banalissimo oggetto che data un HttpServletRequest estrae i dati necessari e li rende disponibili mediante i metodi accessor.
Nota bene: L'oggetto deve essere serializable!

Da notare anche l'override del metodo toString in grado di copiare il valore di tutti i campi del DTO in una Stringa.

Il filtro invece dovrà occuparsi della produzione dei messaggi.

Creare un nuovo filtro, magari usando il wizard di netbeans.
Sempre avvalendosi dei wizard di netbeans è possibile generare il automatico il codice per ottenere la connection factory e la coda configurate al passo precedente.
In tal caso cliccare con il tasto destro del mouse sul codice del filtro e selezionare

Enterprise Resources -> Send JMS Message


Configurare correttamente i valori come nell'immagine in modo da referenziare le risorse create sull'application server.

In alternativa è possibile scrivere a mano i seguenti metodi:

 private Message createJMSMessageFormyDestination(Session session, Object messageData) throws JMSException
    {
        ObjectMessage m = session.createObjectMessage((Serializable)messageData);                
        return m;
    }

    private void sendJMSMessageToMyDestination(Object messageData) throws NamingException, JMSException
    {
        Context c = new InitialContext();
        ConnectionFactory cf = (ConnectionFactory) c.lookup("java:comp/env/myQueueFactory");
        Connection conn = null;
        Session s = null;
        try
        {
            conn = cf.createConnection();
            s = conn.createSession(false, s.AUTO_ACKNOWLEDGE);
            Destination destination = (Destination) c.lookup("java:comp/env/myDestination");
            MessageProducer mp = s.createProducer(destination);
            mp.send(createJMSMessageFormyDestination(s, messageData));
        } finally
        {
            if (s != null)
            {
                s.close();
            }
            if (conn != null)
            {
                conn.close();
            }
        }
    }

generati in automatico da netbeans.

riepilogando il codice di invio del messaggio si eseguono seguenti step:

  1. Lookup della risorsa ConnectionFactory
  2. creazione della connessione
  3. creazione della sessione
  4. Lookup della destination
  5. Creazione del client produttore
  6. Invio messaggio
  7. Chiusura sessione e connessione

Quindi all'interno del metodo principale del filtro, doFilter(ServletRequest request, ServletResponse response,                FilterChain chain)

è possibile richiamare il metodo di invio del messaggio

sendJMSMessageToMyDestination(new RequestDTO(request));

Il contenuto del messaggio sarà l'oggetto request di tipo HttpServletRequest.
Tale oggetto contiene tutte le informazioni utili alla scrittura su log necessaria su MDB.

Test della produzione dei messaggi

Prima di generare il client in grado di consumare i messaggi prodotti dal filtro, è opportuno verificare quantomeno se funziona la produzione, quindi lanciare il progetto EAR e chiamare la servlet di test sulla quale è mappato il filtro.

Potrebbe verificarsi il seguente problema:

javax.naming.NameNotFoundException: myQueueFactory not found
        at com.sun.enterprise.naming.TransientContext.doLookup(TransientContext.java:216)
        at com.sun.enterprise.naming.TransientContext.lookup(TransientContext.java:188)

Ciò indica che il valore JNDI utilizzato in fase di lookup è erroneo.
Nel nostro caso ad esempio netbeans di default considera le risorse trovarsi nel percorso JNDI java:comp/env/nomerisorsa, durante la creazione però glassfish ha inserito le risorse nella root, quindi bisogna apportare le seguenti modifiche in fase di lookup:

ConnectionFactory cf = (ConnectionFactory) c.lookup("myQueue");

Destination destination = (Destination) c.lookup("myDestination");



Client che consuma i messaggi - L'MDB

All'interno del progetto EJB, creiamo un MDB.

Ancora una volta è possibile usare i wizard di netbeans.


Oppure scrivere a mano il seguente codice sorgente:

package MDB;

import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;

@MessageDriven(mappedName = "myDestination", activationConfig =  {
        @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
    })

public class myMDBBean implements MessageListener {

   
    public myMDBBean() {
    }

    public void onMessage(Message message) {
    }
   
}



Grazie all'uso della dependency injection con poche annotazioni si riesce a istanziare il client JMS (ovvero l'MDB).

@MessageDriven(mappedName = "myDestination", activationConfig =  {

        @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),

        @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")

    })

Si indica quindi che il bean è MessageDriven e si aggiungono un paio di proprietà per indicare la tipologia di destinazione (coda o topic) e la destinazione stessa(configurata precedentemente dall'amministratore di sistema).
Con solo un altro step si termina la configurazione dell'MDB, infatti è necessario implementare l'interfaccia MessageListener
public class myMDBBeanimplements MessageListener {
public void onMessage (Message msg) {
FileWriter fw = null;
try
{
ObjectMessage m = (ObjectMessage) message;//casting per estrarre la corretta tipologia di messaggio
RequestDTO requestDTO = (RequestDTO) m.getObject();//estrazione dei dati dal messaggio

//apertura e scrittura su file, chiusura
fw = new FileWriter("c:\\logmdb.txt", true); //occhio al path per i sistemi unix.
fw.append(requestDTO.toString());

} catch (IOException ex)
{
Logger.getLogger(myMDBBean.class.getName()).log(Level.SEVERE, null, ex);
} catch (JMSException ex)
{
Logger.getLogger(myMDBBean.class.getName()).log(Level.SEVERE, null, ex);
} finally
{
try
{
fw.close();
} catch (IOException ex)
{
Logger.getLogger(myMDBBean.class.getName()).log(Level.SEVERE, null, ex);
}
}

}

Conclusione

L'esempio è in grado di elaborare dei dati senza "far perdere tempo" alla servlet in modo tale da scriverli in un file di log.
Lanciando infatti 'n' volte la servlet sarà possibile notare l'inserimento di n righe all'interno del file di log.


nota: Possibili Problemi
Se nel file server.log di glassfish notate la seguente stringa

DirectConsumer:Caught Exception delivering messagecom.sun.messaging.jmq.io.Packet cannot be cast to com.sun.messaging.jms.ra.DirectPacket

sappiate che si tratta di un know issue https://glassfish.dev.java.net/issues/show_bug.cgi?id=3988

Con la tecnologia JMS anche se il client consumatore non è attivo al momento dell'invio del messaggio i messaggi saranno comunque consegnati in seguito.
Per dimostrare ciò, è possibile

  • cancellare temporanemante l'MDB dal progetto;
  • cancellare il file di log "logmdb.txt"
  • effettuare il build e redeploy del progetto sul server
  • lanciare la servlet di test (che genererà un messaggio ad ogni chiamata).

  • ricreare l'MDB
  • effettuare il build e redeploy del progetto sul server

SENZA lanciare la servlet sarà possibile notare che nel log sono state inserire le righe relative alle chiamate effettuate quando l'MDB 
non era completamente presente.


No comments: