Ejb3 In Action Cap4

In questo capitolo vi sono due parti:

  1. la prima legata ai messaggi JSM, che viene solitamente usata per accodare un messaggio in una coda.
  2. la secondo agli MDBs che usano al loro interno JMS, che viene usata per prelevare i messaggi dalla coda.

Oltre alle cose da codice bisogna definire la coda nell'application server su jboss io ho usato questo codice xml mettendolo della home di deploy server/default/deploy/geodrop-destination-service.xml

<?xml version="1.0" encoding="UTF-8"?>
 
<server>
 
    <mbean code="org.jboss.jms.server.destination.QueueService"
 
          name="jboss.messaging.destination:service=Queue,name=geodropSendMailQueue"
 
          xmbean-dd="xmdesc/Queue-xmbean.xml">
 
          <attribute name="JNDIName">/queue/geodrop/sendMailQueue</attribute>
 
          <depends optional-attribute-name="ServerPeer">jboss.messaging:service=ServerPeer</depends>
 
          <depends>jboss.messaging:service=PostOffice</depends>
 
       </mbean>
</server>

Occhio al jndi name della coda che va nel codice java tramite parametro dell'iniection.
Vi è anche un modo per metterlo all'interno dell'ear (o in un altro posto), ma va settato un file xml di jboss che dice di andare a guardare posto da noi definito.

4.1 Messaging concepts

Quando si parla di messagging in un contesto Java EE si intende basso accoppiamento e asincronia delle operazioni. La maggior parte delle operazioni infatti sono sincrone come per esempio tutte le Java RMI viste fin'ora.
Lo strato che gestisce questi messaggi si chiama Message-oriented middleware (MOM).

4.1.3 Messaging models

Vediamo due popolari modelli dei messaggi in contesto Java EE

Point-to-point PTP:

Il messaggio viene mandato in una coda e verrà preso da un ricevitore. Il messaggio non ha destinatario e la coda non garantisce l'ordine di ricezione, inoltre il messaggio viene ricevuto una polta sola

Publish-subscribe (pub-sub)

Il messaggio può avere più destinatari (funzionano un po' come i newsgroup internet) sono adatti ai messaggi di broadcast

4.2 Introducing Java Messaging Service

4.2.1 Developing the JMS message producer

Qui sotto vediamo il codice che richiama l'invio dei messaggi nella coda. Per funzionare senza altri parametri che quelli indicati nel codice bisogna definirlo in un Session Bean. Quindi nella classe dove viene implementato va anteposta l'annotazione @Stateless (anche la @Stateful dovrebbe andare bene ma non l'ho provata), e quando si dichiara bisogna mettere l'annotazione @EJB (senza dimenticarsi che l'ejb ha bisogno della definizione dell'interfaccia). Vediamo un riassunto dell'EJB prima del codice del JMS.

@Stateless
public class MailEnqueuer implements MailEnqueuerLocal
{
.........
@EJB private MailEnqueuerLocal mail;
.......
mail.enqueue(subject, messageBody, recipient, recipientName);
/**  Injects connection factory and destination**/
@Resource(name="jms/QueueConnectionFactory")
 private ConnectionFactory connectionFactory;
@Resource(name="jms/ShippingRequestQueue")           
 private Destination destination;
/*****/ 
 
/** Connects, creates session, producer **/                                                    
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(true,Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(destination);
/*******/
 
/** Creates message **/
ObjectMessage message = session.createObjectMessage();
 
/** Creates payload**/
ShippingRequest shippingRequest = new ShippingRequest();
shippingRequest.setItem(item);
shippingRequest.setShippingAddress(address);
shippingRequest.setShippingMethod(method);
shippingRequest.setInsuranceAmount(amount);
/*****/
 
/** Sets payload **/
message.setObject(shippingRequest);
 
/** Sends message **/              
producer.send(message);
 
session.close();
connection.close();

Retrieving the connection factory and destination

In jms, gli administrative objects sono risorse create, configurate e memorizzate fuori il codice nel JNDI.
JMS ha due oggetti di amministrazione javax.jms.ConnectionFactory and javax.jms.Destination, entrambi usati nel listato sovrastante. Usiamo per recuperare la connection factory l'annotazione @Resource e questa incapsula tramite injection tutte le informazioni di configurazione per connettersi al MOM. Si inietta anche la coda ShippingRequestQueue, con EJB 3 la connessione è molto più semplice infatti non si deve mettere mano al JNDI.

Opening the connection and session

javax.jms.Connection rappresenta la connessione live di MOM che viene creata al momento dell'esecuzione di createConnection. Le connessioni sono thread-safe e possono essere condivise, questo perchè aprire una connessione è un'operazione onerosa.
JMS session (javax.jms.Session) è una operazione single-thread per mandare e ricevere messaggi, si fa attraverso il metodo createSession.
Si è deciso che la sessione deve essere transazionale che il parametro di default deve essere a true. Questo vuol dire che il messaggio non verrà mandato finchè sulla sessione non viene effettuata una commit o la sessione non viene chiusa. Se la sessione non è transazionale il messaggio viene mandato al momento dell'invocazione del send.
Il secondo parametro del metodo createSession la modalità di acknowledge e ha effetto per sessioni nontransazionali che si discuteranno dopo. Dopo aver settato la sessione possiamo mandare il messaggio.

Preparing and sending the message

Mandiamo un oggetto che sia Serializeble. Dopo che abbiamo preparato l'oggetto possiamo mandarlo come payload usando setObject. Infine si manda il messaggio usando il metodo send.

Releasing resources

Sono state allocate molte risorse sotto il cofano per gli oggetti di connessione e di sessione

4.2.2 The JMS message interface

Analizziamo la struttura dell'interfaccia del messaggio, come si vede in figura ci sono diverse parti.
jms-message.jpg

Message headers

Gli headers sono delle coppie nome valore comuni a tutti i messaggi. Sono le informazioni standards per esempio JMSTimestamp è il timestamp , altri headers sono JMSCorrelationID, JMSReplyTo, JMSMessageID

Message properties

Sono delle coppie nome valore definite dall'applicazione, accompagnano il contenuto della busta del messaggio ecco come settarle message.setBooleanProperty("Fragile", true); una proprietà può essere boolean, byte, double, float, int, long, short, String, or Object.

Message body

Il corpo del messaggio, nell'esempio precedente abbiamo scelto javax,jms.OnjectMessage perché abbiamo mandato un oggetto java. Si possono usare anche BytesMessage, MapMessage, StreamMessage, or TextMessage. Ognuno di questi ha delle leggere differenze nell'interfaccia e nel metodo di uso.

4.3 Working with message-driven beans

Gli MDBs possono manipolare diversi tipi di messaggi noi ci focalizzeremo sui messaggi JMS perchè sono tra i più usati nelle applicazioni enterprise. A questo proposito c'è il piccolo sotto-paragrafo definito qui sotto

JCA connectors and messaging

JMS è il provider primario per MDBs ma non è l'unico. Grazie alla Java EE Connector Architecture (JCA) , MDBs possono ricevere messaggi da ogni enterprise information system (EIS), come People-Soft HR or Oracle Manufacturing, non solo MOMs che supporta JMS.

4.3.1 Why use MDBs?

Gli MDBs hanno avuto dei riconoscimenti anche nei momenti difficili di EJB2 . Ecco qui alcune ragioni.

Multithreading

Come si vede in figura vi è un pool di thread che recupera i messaggi dalla coda, tutto questo codice per i thread è gestito in maniera trasparente dal sistema e lo sviluppatore non se ne deve preoccupare.
mdb-pool.jpg

Simplified messaging code

Molto del codice di configurazione viene demandato al sistema, basta definire l'annotazione giusta o il deployment descriptor.

Starting message consumption

Per consumare un messaggio che sta nella coda non conviene invocare un metodo dell'applicazione. Il meccanismo deve essere automatizzato dal server.
Si vedranno maggiormente nel dettaglio questi punti in seguito.

4.3.2 Programming rules

Come tutti gli EJB anche gli MDBs sono dei plain objects con annotazioni. Vi sono delle semplici regole da tenere a mente.

  • La classe MDB: deve direttamente (tramite implements) o indirettamente (attraverso annotazione o descriptors) implementare una interfaccia di message listenener, non può essere abstract, deve essere una POJO e non una sottoclasse di un altra MDB, deve essere pubblica.
  • La classe di bean: non deve avere argomenti nel costruttore (se non si definisce il costruttore il container ne creerà uno) , non si può definire un metodo finalize se si ha bisogno di codice di cleanup deve essere designato un metodo di PreDestroy, deve implementare metodi definiti nell'interfaccia di listener che devono essere pubblic e static, non si può tirare una javax.rmi.RemoteException or any runtime exceptions. If a RuntimeException is thrown, the MDB instance is terminated.

4.3.3 Developing a message consumer with MDB

Vediamo un esempio.

package ejb3inaction.example.buslogic;
import javax.ejb.MessageDriven;
import javax.ejb.ActivationConfigProperty;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import java.sql.*;
import javax.sql.*;
 
/***  Define @MessageDriven annotation ***/
@MessageDriven(
    name="ShippingRequestProcessor",
    activationConfig = {
        @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
    @ActivationConfigProperty( propertyName="destinationName", propertyValue="jms/ShippingRequestQueue")
    }
)
/*******/
 
// Implements message listener  
public class ShippingRequestProcessorMDB implements MessageListener {
    private java.sql.Connection connection;
    private DataSource dataSource;
 
    //Injects MessageDrivenContext
    @Resource
    private MessageDrivenContext context;
 
   //Uses resource injection 
   @Resource(name="jdbc/TurtleDS")
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
 
/*** Implements onMessage method ***/
    @PostConstruct
    public void initialize() {
        try {
            connection = dataSource.getConnection();
        } catch (SQLException sqle) {
            sqle.printStackTrace();                 
        }
    }
    @PreDestroy
      public void cleanup() {
      try {
          connection.close();
          connection = null;
      } catch (SQLException sqle) {
          sqle.printStackTrace();
      }
  }
 
  //Defines lifecycle callbacks
  public void onMessage(Message message) {
      try {
          ObjectMessage objectMessage = (ObjectMessage)message;
          ShippingRequest shippingRequest = (ShippingRequest)objectMessage.getObject();
          processShippingRequest(shippingRequest);
      } catch (JMSException jmse) {
          jmse.printStackTrace();
          context.setRollBackOnly();();
      } catch (SQLException sqle) {
          sqle.printStackTrace();
          context.setRollBackOnly();
      }
  }
 
  //Processes business logic
  private void processShippingRequest(ShippingRequest request) throws SQLException {
      Statement statement = connection.createStatement();
      statement.execute( 
          "INSERT INTO "
              + "SHIPPING_REQUEST ("
              + "ITEM, "
              + "SHIPPING_ADDRESS, "
              + "SHIPPING_METHOD, "
              + "INSURANCE_AMOUNT ) "
              + "VALUES ( "
              + request.getItem() + ", "
              + "\'" + request.getShippingAddress() + "\', "
              + "\' " + request.getShippingMethod() + "\', "
              + request.getInsuranceAmount() + " )");
  }
}

Define @MessageDriven annotation: identifica l'oggetto come MDB e specifica la configurazione includendo la coda dove si sta ascoltando (sembra tipo di coda e nome per identificarla).
Implements message listener , deve implementare come da regole. Il metodo onMessage necessario per la corretta implementazione processa i messaggi in ingresso, anche l'init deve essere implementato.
Viene iniettato un message-driven context e una risorsa di db.
Vediamo più in dettaglio le annotazioni.

4.3.4 Using the @MessageDriven annotation

Guardiamo il codice di questa annotazione. Tutti e tre gli argomenti sono opzionali.

@Target(TYPE)
@Retention(RUNTIME)
public @interface MessageDriven {
    String name() default "";
    Class messageListenerInterface default Object.class;
    ActivationConfigProperty[] activationConfig() default {};
       String mappedName();
    String description();
}

Il primo argomento name specifica il nome dell'MDB, se viene omesso verrà usato quello della classe. messageListenerInterface specifica quale listener si deve implementare, activationConfig specifica le proprietà di configurazione.

4.3.5 Implementing the MessageListener

Il container usa l'interfaccia di listener per registrare l'MDB con il message provider e per passare i messaggi in entrate tramite il metodo implementato. Si può indicare il listener tramite annotazione o tramite implements nella classe o attraverso il deployment descrittor.

@MessageDriven(
    name="ShippingRequestJMSProcessor",
    messageListenerInterface="javax.jms.MessageListener")
public class ShippingRequestProcessorMDB {
//OPPURE
public class ShippingRequestProcessorMDB implements MessageListener {

Questo tipo di possibilità ci permette di sfruttare parte del codice dell'MDB se dovessimo fare un cambiamento tra JMS e JAXM (soap based xml messaging) . Basta cambiare il listener e mettere quello nuovo javax.jaxm.OneWayMessageListener. Ovviamente poi si dovrà fornire una valida implementazione dei metodi.

4.3.6 Using ActivationConfigProperty

Come abbiamo visto sopra permette di passare i parametri di configurazione attraverso un array.
Ecco la definizione, come si vede sono coppie nome valore.

public @interface ActivationConfigProperty {
    String propertyName();
    String propertyValue();
}

Per capire meglio come funziona vediamo un esempio:
@MessageDriven(
    name="ShippingRequestProcessor",
    activationConfig = {
        @ActivationConfigProperty(
            propertyName="destinationType",
            propertyValue="javax.jms.Queue"),
        @ActivationConfigProperty(
            propertyName="connectionFactoryJndiName",
            propertyValue="jms/QueueConnectionFactory"
        ),
        @ActivationConfigProperty(
            propertyName="destinationName",
            propertyValue="jms/ShippingRequestQueue")
    }
)
  • La proprietà destinationType dice al container che questo MDB sta ascoltando in una coda. Se stessimo ascoltando un topic invece, il valore sarebbe javax.jms.Topic .
  • connectionFactoryJndiName specifica il JNDI name usato per creare la connessione.
  • destinationName specifica che noi stiamo ascoltando per messaggi che arrivano a destinazione con il nome jms/ShippingRequestQueue

Vi sono altri parametri per avere un maggiore dettaglio della cosa.

acknowledgeMode

I messaggi non vengono rimossi dalla coda finchè non viene mandato un acknowledge, vi sono diversi tipi. Quello di default che viene usato se non si specifica niente e di solito va bene per molti casi è AUTO_ACKNOWLEDGE indica che i messaggi di acknowledge vengono mandati per conto nostro in background. Per cambiare il tipo si usa questo

@ActivationConfigProperty(
    propertyName="acknowledgeMode",
    propertyValue="DUPS_OK_ACKNOWLEDGE")

Table 4.1 JMS session acknowledge modes. For nontransacted sessions, you should choose the
mode most appropriate for your project. In general, AUTO_ACKNOWLEDGE is the most common and
convenient. The only other mode supported with MDB is DUPS_OK_ACKNOWLEDGE.
ackonwledge-modes.jpg

subscriptionDurability

Se il nostro MDB è in ascolto su un topi possiamo specificare se la subscription topic è durabile o meno.
???? Non ho capito il resto ????

messageSelector

Questa proprietà serve per applicare un filtro ai messaggi del JMS consumer. I criteri di selezione vengono applicati agli header e alle proprietà del messaggio, specificando quali messaggi il consumer vuole ricevere. Nel esempio vogliamo ricere tutti i messaggi dove la proprietà Fragile è a TRUE.

MessageConsumer consumer = session.createConsumer(destination,"Fragile IS TRUE");

La sintassi è simile a quella dell'SQL e la selezione può essere anche molto complessa. Possono essere incluse literals, identifiers, whitespace, expressions, standard brackets, logical and comparison operators, arithmetic operators, and null comparisons.
@ActivationConfigProperty( propertyName="messageSelector", propertyValue="Fragile IS TRUE")

selector-syntax.jpg

4.3.7 Using bean lifecycle callbacks

Il ciclo di vita è similare ai sessions stateless bean:

  1. Il bean viene creato.
  2. Gli vengono iniettate le risorse necessarie.
  3. Viene posto nel pool
  4. Viene tirato fuori dal pool all'arrivo di una richiesta
  5. Viene eseguito il metodo onMessage
  6. Alla fine dell'esecuzione di onMessage viene riposto nuovamente nel pool in stato di ready
  7. Se c'è bisogno (riduzione della dimensione del pool o altro) viene distrutto.

Vi sono due metodi di callback chiamati durante il ciclo di vita dell'MDB. PostCostruct chiamato dopo la creazione e l'iniezione delle risorse e PreDestroy chiamato prima che l'istanza venga ritirata e rimossa dal bean. Questi metodi sono usati per allocare e rilasciare risorse che poi sono usati dentro il metodo onMessage.
Nell'esempio visto prima durante onMessage veniva aperta una connessione al db dove veniva effettuata la insert. Questa è una tipica risorsa pesante che deve essere rilasciata.
L'iniezione avviene tramite JNDI attraverso questo codice

@Resource(name="jdbc/TurtleDS")
public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
}

Dopo l'iniezione il container controlla se ci sono metodi da eseguire prima che l'MDB venga messo nel pool. Per dichiarare un metodo di PostCostruct vi è l'annotazione
@PostConstruct
public void initialize() {
.....................
@PreDestroy
public void cleanup() {

4.3.8 Sending JMS messages from MDBs

Abbiamo visto nel primo listato come mandare messaggi JMS, con gli MDBs si può fare in modo più semplice e robusto. Possiamo iniettare la coda jms/ShippingErrorQueue e la connection factory named jms/QueueConnectionFactory usando l'annotazione @Resource.

@Resource(name="jms/ShippingErrorQueue")
private javax.jms.Destination errorQueue;
@Resource(name="jms/QueueConnectionFactory")
private javax.jms.ConnectionFactory connectionFactory;

Possiamo creare e distruggere una istanza di javax.jms.Connection usadando le callbacks come avevamo fatto con la connessione JDBC nel paragrafo precedente.
@PostConstruct
public void initialize() {
    ...
    jmsConnection = connectionFactory.createConnection();
    ...
}
@PreDestroy
public void cleanup() {
    ...
    jmsConnection.close();
    ...
 }

Il metodo di buisness che manfa messaggi di errore è come il resto del codice visto all'inizio
private void sendErrorMessage(ShippingError error) {
    Session session = jmsConnection.createSession(true,
        Session.AUTO_ACKNOWLEDGE);
    MessageProducer producer = session.createProducer(errorQueue);
    ...
    producer.send(message);
    session.close();
}

4.3.9 Managing MDB transactions

Il container inizia la transazione quando viene lanciato il metodo onMessage e si fa il commit della stessa quando il metodo finisce a meno che la transazione non viene marcata come roolback attraverso message-driven context

4.4 MDB best practices

  • Scegliere il modello dei messaggi attentamente: il modello dei messaggi dovrebbe indipendente dal dominio.
  • Ricordarsi la modularizzazione. La buisness logic deve essere disaccoppiata e modularizzata lontana dai metodi specifici riguardanti il messaggio. Nell'esempio visto onMessage richiamava un altro metodo processShippingRequest per compiere le azioni necessarie. Un altra pratica è quella di mettere la logica in un session bean e invocarla dal metodo di onMessage.
  • Fare un buon uso del filtro dei messaggi: se si usa la stessa coda per due tipi di comunicazioni conviene usare i selettori. Altrimenti definire due code.
  • Scegliere il tipo dei messaggi attentamente: un modo parecchio usato è quello di usare messaggi XML, questo è buono per ridurre l'accoppiamento ma fa crescere di molto la dimensione del messaggio causando un degrado delle performance. Agli antipodi vi è quella di usare degli stream binari per comunicare.
  • Stare attenti ai messaggi avvelenati: per esempio un messaggio che non è un ObjectMessage. Lo tratteremo con questo codice:
try {
    ObjectMessage objectMessage = (ObjectMessage)message;
    ShippingRequest shippingRequest = (ShippingRequest)objectMessage.getObject();
    processShippingRequest(shippingRequest);
} catch (JMSException jmse) {
    jmse.printStackTrace();
    context.setRollBackOnly();
}

Questo causerà al momento del cast una eccezione di tipo java.lang.ClassCastException. Finché onMessage non finirà correttamente non verrà spedito l'acknowledge e verrà fatto un roll-back transazione rimettendo il messaggio nella coda. Ma noi rileggeremo lo stesso messaggio andando in un loop infinito. I messaggi che causano questi problemi sono detti messaggi avvelenati.
Fortunatamente molti MOMs e EJB containers includono un conteggio dei redelivery e delle code di messaggi morti. Quindi dopo un numero di volte che viene trattato verrà rimosso dalla coda e messo in un altra detta dead-queue. Il meccanismo dipende dal vendor e non è standard.
  • Configurare la dimensione del pool correttamente. Troppo piccolo porta a lentezza troppo grande spreca risorse.
Salvo diversa indicazione, il contenuto di questa pagina è sotto licenza Creative Commons Attribution-ShareAlike 3.0 License