Laborator 09

Comunicații Asincrone folosind Java Message Service

Obiective

  • configurarea serverului de aplicații Glass Fish 4.1 pentru gestiunea unor obiecte administrate (fabrici de conexiuni, destinații) implicate în transferul de mesaje folosind API-ul JMS;
  • înțelegerea arhitecturii interfeței de programare Java Message Service;
  • descrierea mecanismelor prin care pot fi transmise mesaje folosind API-ul JMS;
  • utilizarea tipurilor de obiecte implementate în cadrul interfeței de programare Java Message Service;
  • folosirea unor funcționalități avansate (tratare excepții, specificarea unor timpi de expirare, tranzacții) pentru gestiunea cazurilor speciale;
  • proiectarea și dezvoltarea unei aplicaţii conţinând mai multe componente care comunică asincron prin intermediul unor mesaje care respectă specificația JMS, furnizorul de servicii fiind un server de aplicații Glass Fish 4.1;

Cuvinte Cheie

Java Message Service, peer-to-peer, publish-subscribe, loosely coupled systems, message, topic, queue, connection factories, destinations, JNDI, resource injection

Materiale Ajutătoare

Java Message Service - aspecte generale

Un sistem de gestiune al mesajelor este un mecanism de comunicare între componente ale unei aplicaţii. De regulă, este un sistem punct la punct (eng. peer-to-peer) astfel încât un client poate primi mesaje şi poate transmite mesaje de la şi către orice alt client. Fiecare client se conectează la un agent responsabil cu gestiunea mesajelor ce oferă funcţionalităţi pentru crearea lor, transferul prin infrastructura de comunicaţie şi realizarea diferitelor operaţii ce le implică.

Prin intermediul unui sistem de gestiune al mesajelor, se permite dezvoltarea de aplicaţii distribuite slab cuplate, astfel încât o componentă transmite un mesaj către o adresă în timp ce altă componentă primeşte un mesaj de la adresa respectivă, aceasta fiind singurul element pe care îl au în comun, comunicaţia nefiind condiţionată de disponibilitatea simultană a acestora. Mai mult, componenta care transmite mesajul nu trebuie să ştie nimic despre componenta care îl primeşte şi nici invers. Acestea trebuie să cunoască numai formatul pe care trebuie să îl respecte mesaje precum şi adresa la care, respectiv de la care să fie transferate.

Din acest punct de vedere, sistemul de transmitere al mesajelor se deosebeşte de tehnologiile strâns cuplate (cum ar fi comunicaţiile la distanţă de tip RPC) care solicită aplicaţiei să cunoască funcţionalităţile puse la dispoziţie precum şi modul în care acestea pot fi accesate. Totodată, sistemul de transmitere al mesajelor se deosebeşte şi de sistemul de poştă electronică, metodă ce presupune interacţiunea între persoane (sau între aplicaţii şi persoane), întrucât acesta implică comunicaţia între componente ale unor aplicaţii.

Java Message Service (JMS) este un API Java care permite aplicaţiilor crearea de mesaje, transferul lor prin infrastructura de comunicaţie (operaţii de transmitere şi primire) şi prelucrarea acestora. Sunt puse la dispoziţie şi un set de interfeţe cu semantica asociată ce permit programelor scrise folosind limbajul de programare Java să comunice cu alte implementări ale unor sisteme pentru gestiunea mesajelor. De asemenea, este redus sistemul de concepte pe care programatorul trebuie să şi le însuşească astfel încât să poată dezvolta produse care implică transferul de mesaje, oferind suficiente funcţionalităţi pentru realizarea de aplicaţii complexe. Se încearcă şi asigurarea portabilităţii aplicaţiilor care utilizează JMS între diferiţi producători care implementează acest API.

Comunicaţia în cazul Java Message Service este caracterizată, pe lângă slaba cuplare a componentelor ce interacţionează şi prin:

  • asincronicitate: o componentă nu trebuie să primească în mod necesar mesajele la acelaşi moment în care acestea au fost transmise; o aplicaţie poate transmite mesajele, realizând ulterior alte operaţii în timp ce programul care trebuie să le primească poate îndeplini alte sarcini înainte de a le primi;
  • siguranţa transferului întrucât furnizorul serviciilor de gestiune a mesajelor garantează că un mesaj este transmis în mod cert o singură dată.
Sunt disponibile în acelaşi timp şi alte nivele de siguranţă a transferului în cazul în care aplicaţiile nu sunt afectate de pierderea unor mesaje, respectiv de primirea unor duplicate.

În prezent, versiunea specificaţiei JMS este 2.0.

Este probabil ca un dezvoltator de aplicaţii integrate pentru întreprinderi să aleagă un sistem de gestiune al mesajelor în detrimentul unor componente strâns cuplate în situaţia în care doreşte ca funcţionalitatea modulelor să nu depindă de alte informaţii puse la dispoziţie de interfeţele altor aplicaţii (astfel încât procesul de întreţinere să se poată realiza cu uşurinţă, înlocuindu-se anumite componente şi păstrând pe altele), ca programul să funcţioneze şi atunci când unele module nu rulează simultan sau dacă fluxul operaţional permite ca ulterior transmiterii unor mesaje componentele pot opera indiferent dacă au primit sau nu un răspuns.

Într-o aplicaţie integrată pentru întreprinderi, un sistem de mesagerie poate fi utilizat pentru interacţiunea dintre componente, astfel că unele operaţii să poată fi realizate automat prin prelucrarea mesajelor de îndată ce modulul căruia îi sunt adresate devine disponibil.

Scopul Java Message Service, atunci când a fost dezvoltat, a constat în a oferi aplicaţiilor Java un mecanism de a accesa sistemele orientate pe mesaje existente (eng. Middleware Message-Oriented Systems). De atunci, distribuitorii au adoptat şi implementat acest model, astfel că un produs de acest tip oferă funcţionalităţi complete de mesagerie pentru o organizaţie. Acesta este integrat în Java Enterprise Edition, astfel încât poate fi utilizat pentru transfer de mesaje în cadrul acestor componente.

Aplicaţiile client, componentele Enterprise JavaBeans (EJB) şi componentele web pot trimite sau primi asincron un mesaj. Mai mult, aplicaţiile client pot indica un ascultător de mesaje care permit ca mesajele să fie livrate asincron prin intermediul unei notificări atunci când acesta devine disponibil. Componentele orientate pe mesaje (eng. message-driven bean) oferă posibilitatea consumării mesajelor în cadrul unui container EJB, întrucât serverul de aplicaţii le grupează pentru a oferi prelucrarea concurentă a mesajelor. De asemenea, operaţiile de trimitere şi primire a mesajelor pot participa la o tranzacţie Java Transaction API (JTA), care permite ca operaţiile JMS şi accesul la baza de date să se realizeze în mod atomic.

Astfel, JMS a contribuit la îmbunătăţirea altor părţi din cadrul platformei Java EE, simplificând procesul de dezvoltare al aplicaţiilor, facilitând interacţiunea dintre programele dezvoltate utilizând această tehnologie şi sistemele moştenite responsabile cu gestiunea mesajelor. Furnizorul JMS poate fi integrat cu un server de aplicaţii folosind arhitectura Java EE Connector, accesul realizându-se printr-un adaptor de resuse, astfel că se permite interacţiunea între distribuţiile JMS şi diferite servere de aplicaţii.

Un programator poate implementa cu uşurinţă funcţionalităţi noi unei aplicaţii Java EE pentru care au fost definite evenimente legate de fluxurile operaţionale şi procesele de afaceri prin dezvoltarea unei componente orinetate pe mesaje care le poate procesa. Şi platforma Java EE îmbunătăţeşte API-ul JMS prin suportul pentru tranzacţii JTA şi consumarea concurentă a mesajelor.

Structura unui mesaj în Java Message Service

Scopul unei aplicaţii JMS îl reprezintă producerea şi consumarea de mesaje care să poată fi utilizate de diferite componente. Mesajele definite de API-ul JMS au un format flexibil care permite specificarea unei structuri compatibile diverselor programe ce rulează pe diferite platforme.

Un mesaj JMS (definit de clasa javax.jms.Message) are trei părţi:

  • un antet (obligatoriu);
  • proprietăţi;
  • un conţinut.

Antetul unui mesaj JMS conţine un număr de câmpuri predefinite care conţin valori utilizate atât de clienţi cât şi de furnizorii serviciului de mesagerie pentru a identifica mesajele şi pentru a le direcţiona către componentele dorite. Fiecare atribut din cadrul antetului are definite metode de tip setter şi getter. Pentru unele, valorile sunt stabilite de către client, însă pentru cele mai multe valorile sunt indicate în mod automat de furnizorul serviciului de mesagerie (prin intermediul metodei send()), suprascriind datele specificate.

Atribut Descriere Componenta care îi stabileşte valoarea
JMSDestination destinaţia la care este transmis mesajul metoda send() a furnizorului de servicii de mesagerie
JMSDeliveryMode mecanismul de transfer specificat atunci când este transmis mesajul metoda send() a furnizorului de servicii de mesagerie
JMSDeliveryTime momentul de timp la care a fost transmis mesajul la care se adaugă întârzierea pentru livrare indicată la transmiterea mesajului metoda send() a furnizorului de servicii de mesagerie
JMSExpiration perioada de expirare a mesajului metoda send() a furnizorului de servicii de mesagerie
JMSPriority prioritatea mesajului metoda send() a furnizorului de servicii de mesagerie
JMSMessageID valoare care identifică în mod unic fiecare mesaj transmis de furnizorul de servicii de mesagerie metoda send() a furnizorului de servicii de mesagerie
JMSTimestamp momentul de timp la care mesajul a fost livrat furnizorului de servicii de mesagerie pentru a fi transmis metoda send() a furnizorului de servicii de mesagerie
JMSCorrelationID valoare prin care un mesaj referă un altul prin indicarea atributului JMSMessageID aplicaţia client
JMSReplyTo locaţia la care ar trebui transmise răspunsurile la mesaj aplicaţia client
JMSType identificatorul cu privire la tip furnizate de aplicaţia client aplicaţia client
JMSRedelivered valoare care indică dacă mesajul este retransmis furnizorul de servicii de mesagerie (înainte de transmitere)

Utilizatorul poate specifica un set de proprietăţi pentru mesaje dacă sunt necesare atribute suplimentate pentru care să se specifice valori în plus faţă de cele oferite. Acestea pot fi utilizate pentru a oferi compatibilitate cu alte sisteme de mesagerie sau pot fi folosite pentru a folosi selectoare de mesaje.

API-ul JMS oferă şi unele nume de proprietăţi predefinite, prefixate de JMSX. Un furnizor de servicii de mesagerie trebuie să implementeze doar una dintre acestea, JMSXDeliveryCount (care indică de câte ori a fost transmis un mesaj), restul fiind opţionale.

Folosirea proprietăţilor predefinite sau a celor definite de utilizator este opţională.

În cadrul API-ului JMS sunt definite mai multe tipuri de mesaje, fiecare având un conţinut specific, permiţând prelucrarea datelor în formaturi diferite.

Tip de Mesaj Conţinut
TextMessage un obiect java.lang.String
MapMessage un set de perechi (atribut, valoare), atributul fiind obiect de tip java.lang.String iar valoarea un tip primitiv de date; valorile pot fi accesate fie secvenţial printr-un iterator / enumerator (ordinea nefiind definită), fie aleator prin denumirea atributului
BytesMessage un flux de octeţi neinterpretaţi; acest tip de mesaj este folosit pentru codificarea unui conţinut astfel încât să corespundă unui format de mesaj existent
StreamMessage un flux de valori primitive, specificate şi accesate secvenţial
ObjectMessage un obiect serializabil (ce implementează java.io.Serializable) definit în limbajul de programare Java
Message vid, mesajul fiind compus doar din antet şi proprietăţi, acesta fiind util atunci când se doreşte semnalizarea unui eveniment

Apelul context.createProducer().send(receiver, context.createMessage()); transmite un astfel de mesaj. O situaţie în care se transmite un astfel de mesaj este cea în care se semnalează că toate mesajele au fost transmise.

Sunt oferite metode pentru construirea de mesaje de fiecare tip, stabilindu-se conţinutul specific. Un obiect de tip TextMessage poate fi creat astfel:

TextMessage message = context.createTextMessage();
message.setText(content);
context.createProducer().send(message);

Mesajul va fi transmis ca un obiect generic de tip Message, acesta trebuind convertit la tipul corespunzător, folosindu-se metode specifice pentru accesarea conţinutului (respectiv a antetelor şi a proprietăţilor, dacă este necesar).

Message message1 = consumer.receive();
MapMessage mapMessage = (MapMessage)message1;
Enumeration<String> mapAttributes = mapMessage.getMapNames();
for(String mapAttribute: mapAttributes)
    String mapValue = mapMessage.getObject(mapAttribute);
 
Message message2 = consumer.receive();
ObjectMessage objectMessage = (ObjectMessage)message2;
CustomMessage customMessage = (CustomMessage)objectMessage.getObject();
Spre exemplu, se pot folosi metodele de citire orientate pe fluxuri oferite de tipul de mesaj BytesMessage. Pentru a se obţine conţinutul unui mesaj de tip StreamMessage, trebuie realizată întotdeauna operaţia de convertire.

Alternativ, se poate apela metoda getBody() a clasei Message, indicându-se tipul de mesaj ca argument.

Message message = consumer.receive();
af (message instanceof TextMessage) {
    String content = message.getBody(String.class);
}

API-ul JMS oferă totodată posibilitatea de a crea şi de a primi rapid mesaje de tipul TextMessage, BytesMessage, MapMessage sau ObjectMessage, indicând conţinutul de transmis direct în metoda send(), respectiv prin utilizarea metodei receiveBody():

context.createProducer().send(receiver, content);
String content = consumer.receiveBody(String.class);
Metoda receiveBody() poate fi utilizată pentru a primi orice tip de mesaj cu excepţia StreamMessage şi Message, atâta timp când conţinutul mesajelor poate fi atribuit unui anumit tip de date.

Arhitectura Java Message Service

O aplicaţie JMS este formată din următoarele componente:

  1. furnizorul JMS este un sistem de mesagerie ce implementează interfeţele specificate de API, oferind totodată funcţionalităţi administrative şi de control; o implementare a platformei Java EE care suportă întregul profil include şi un furnizor JMS; de regulă, acesta este reprezentat de un server de aplicaţii;
  2. clienţii JMS sunt aplicaţii sau componente scrise în limbajul de programare Java ce produc şi consumă mesaje; atât programele Java EE cât şi cele Java SE pot implementa un astfel de comportament;
  3. mesajele sunt obiecte care comunică informaţii între clienţii JMS;
  4. obiectele administrate sunt configurate pentru a fi utilizate de clienţii JMS; există două tipuri de obiecte administrate şi anume fabrici de conexiuni (eng. connection factories) şi destinaţii (eng. destinations); administratorul poate crea obiecte care sunt disponibile tuturor aplicaţiilor care folosesc o instanţă a unui server de aplicaţii.
Există şi posibilitatea de a se utiliza adnotări pentru a se crea obiecte care sunt specifice unei anumite aplicaţii.

Interacţiunea dintre componente constă în faptul că obiectele administrate (fabricile de conexiuni şi destinaţiile) se asociază unui spaţiu de nume JNDI (Java Naming and Directory Interface).

Prin intermediul JNDI se permite aplicaţiilor distribuite să caute servicii într-un mod abstract, independent de resurse. Astfel, în cazul în care se schimbă mediul de producţie al unei aplicaţii, aceasta nu trebuie modificată pentru a putea rula, accesând nişte resurse la distanţă. Totodată, sunt ascunse detalii pe care un utilizator ce foloseşte resursa nu ar trebui să le cunoască, acestea fiind configurate la nivelul serverului de aplicaţii.

Un client JMS poate utiliza injectarea resurselor (eng. resource injection) spre a accesa obiectele administrate stabilind o conexiune logică către acestea prin intermediul furnizorului de servicii de mesagerie.

Înainte de dezvoltarea JMS, cele mai multe produse ce implementau sisteme de mesagerie suportau mecanisme de tipul punct la punct respectiv publicare-abonare (eng. publish-subscribe). Un furnizor JMS trebuie să ofere ambele moduri de comunicare, punând la dispoziţia programatorilor interfeţe specifice pentru fiecare dintre acestea.

Prin intermediul API-ului JMS, programatorii nu sunt limitaţi la folosirea unui anumit mecanism exclusiv, permiţându-se folosirea aceluiaşi cod sursă pentru transmiterea şi primirea de mesaje folosind fie modelul punct la punct, fie modelul publicare-abonare. Cu toate că destinaţiile unui mesaj sunt specifice stilului de comunicare folosit, comportamentul aplicaţiei depinzând parţial de utilizarea unei cozi (eng. queue) respectiv a unei teme (eng. topic), codul sursă propriu-zis este comun celor două mecanisme, dând aplicaţiei flexibilitate întrucât aceasta poate fi reutilizată uşor.

O aplicaţie punct la punct este construită pe conceptele de cozi, destinatari şi expeditori. Fiecare mesaj este transmis de un destinatar către o anumită coadă, de unde expeditorii le primesc. Cozile reţin toate mesajele transmise către ele până când acestea sunt primite sau până când expiră.

Caracteristicile unui sistem de mesagerie punct la punct sunt:

  • fiecare mesaj are un singur consumator;
  • destinatarul va primi mesajul indiferent de disponibilitatea sa la momentul de timp la care expeditorul a transmis mesajul.

Utilizarea mecanismului de comunicare punct la punct este limitată la situaţia în care se doreşte ca fiecare mesaj să fie prelucrat de un singur client.

Într-o componentă ce respectă modelul publicare-abonare, clienţii transmit mesajele către o temă, putând realiza operaţii de publicare sau abonare în mod dinamic. Responsabilitatea pentru distribuirea mesajelor de la clienţii care publică mesaje către abonaţi revine sistemului de mesagerie. Tema va reţine mesajele numai în răstimpul necesar pentru a le transfera.

În cazul acestui model, trebuie făcută distincţia între abonatul propriu-zis şi abonamentul pe care acesta îl creează: dacă abonatul este un obiect JMS de tip consumator în cadrul unei aplicaţii, abonamentul este o entitate care există la nivelul furnizorului de servicii de mesagerie. O temă poate avea mai mulţi abonaţi, însă unui abonament îi corespunde, de regulă, un singur client.

Există de asemenea posibilitatea de a se crea abonamente partajate, identificate printr-un anumit şir de caractere, care pot fi folosite de mai mulţi clienţi concomitent însă acestea sunt folosite pentru distribuirea prelucrărilor întrucât un mesaj nu poate fi primit decât de către un singur consumator.

Caracteristicile unui sistem publicare-abonare sunt:

  • fiecare mesaj poate avea mai mulţi consumatori;
  • un client care se abonează la o temă poate consuma doar mesajele transmise după ce a creat abonamentul, el trebuind să fie disponibil pentru a putea consuma în continuare mesaje.
O astfel de cerinţă este însă relaxată de API-ul JMS într-o anumită măsură, permiţând clienţilor să creeze abonamente durabile, care păstrează mesajele transmise în perioada în care consumatorii nu sunt activi. Subscripţiile de acest tip oferă flexibilitate şi asigură siguranţa transmiterii mesajelor pe care le oferă cozile, permiţând în acelaşi timp transmiterea de mesaje către mai mulţi destinatari.

Utilizarea mecanismului de comunicare publicare-abonare este adecvată situaţiilor în care un mesaj poate fi procesat de orice număr de consumatori (inclusiv nici unul).

Cele mai multe produse ce oferă facilităţi de transfer al mesajelor sunt asincrone, în sensul că nu există o dependenţă temporală fundamentală între momentul la care a fost produs mesajul şi momentul la care acesta a fost consumat. Totuşi, specificaţia JMS permite faptul ca un mesaj să fie procesat în ambele moduri:

  • sincron, consumatorul primind mesajul în mod explicit de la destinatar printr-un apel al metodei receive();
Metode receive() se poate bloca până la momentul în care un mesaj este transmis sau până când a expirat o anumită perioadă de aşteptare.
  • asincron, în care aplicaţia client va defini un ascultător de mesaje, a cărui funcţionare este similară ca în cazul evenimentelor: de fiecare dată când se transmite un mesaj, furnizorul de servicii de mesagerie va face ca acesta să fie primit prin apelul metodei onMessage(), ce acţionează asupra conţinutului.
În cazul unei aplicaţii Java EE, o componentă orientată pe mesaje funcţionează pe post de ascultător de mesaje (având definită de asemenea metoda onMessage()), cu diferenţa că aceasta nu trebuie înregistrată la un anumit consumator.

Modelul de programare al API-ului JMS

Principalele componente ale unei aplicaţii JMS sunt:

  • obiectele administrate (fabrici de conexiuni şi destinaţii);
  • conexiuni și sesiuni (reţinute împreună într-un obiect de tipul JMSContext);
  • producătorii de mesaje;
  • consumatorii de mesaje;
  • mesajele propriu-zise.
O fabrică de conexiuni poate fi folosită pentru crearea unui obiect de tipul JMSContext (iniţial este creat obiectul de tip Connection şi din acesta obiectul de tip Session). Acesta este folosit pentru crearea mesajului propriu-zis, dar şi a producătorului de mesaje care transmite mesaje către destinaţie precum şi a consumatorului de mesaje care primeşte mesaje de la destinaţie.

În cadrul unei aplicaţii JMS, obiectele administrate – fabricile de conexiuni şi destinaţiile – sunt întreţinute administrativ mai degrabă decât programatic întrucât tehnologia prin intermediul căruia sunt implementate poate diferi de la o implementare la alta a API-ului. Gestiunea obiectelor administrate face parte din cadrul sarcinilor administrative ce pot varia de la un distribuitor la altul.

Clienţii JMS accesează obiectele administrate prin intermediul unor interfeţe portabile, astfel încât se poate face trecerea între diferite distribuţii fără prea multe modificări.

Configurarea lor se face în contextul unui spaţiu de nume JNDI, astfel că accesul la nivelul clienţilor se face prin injectarea resurselor.

Specificaţia platformei Java EE permite unui programator crearea de obiecte administrate folosind adnotări sau elemente descriptive în cadrul procesului de dezvoltare (eng. deployment descriptor elements). Astfel de obiecte vor fi însă specifice aplicaţiei pentru care au fost create. Definiţiile ale unui element descriptiv în cadrul procesului de dezvoltare le suprascriu pe cele indicate de adnotări.

O fabrică de conexiuni este un obiect utilizat de client pentru a crea o conexiune către un furnizor JMS de servicii de mesagerie. Un astfel de obiect înglobează un set de parametrii ce conţin configurări ale conexiunii, aceştia fiind definiţi de administrator. O fabrică de conexiuni este o instanţă a uneia din interfeţele ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory. Fiecare aplicaţie client JMS începe cu injectarea unei fabrici de conexiuni într-un obiect ConnectionFactory.

queueConnectionFactory = (QueueConnectionFactory) 
               initialContext.lookup(Constants.QUEUE_CONNECTION_FACTORY_NAME);
topicConnectionFactory = (TopicConnectionFactory) 
               initialContext.lookup(Constants.TOPIC_CONNECTION_FACTORY_NAME);

În cazul unei aplicaţii Java EE, fabrica de conexiuni va fi indicată prin intermediul numelui logic JNDI (java:comp/DefaultJMSConnectionFactory, pentru obiectul implicit preconfigurat pe serverul de aplicaţii), folosindu-se adnotarea @Resource cu parametrul lookup:

@Resource(lookup = "java:comp/DefaultJMSConnectionFactory ")
private static ConnectionFactory connectionFactory;

Obiectul de tip InitialContext este creat pe baza unor parametrii care indică mecanismul de conectare la serverul de aplicaţii, inclusiv protocolul de comunicaţii, adresa şi portul.

O destinaţie este un obiect folosit de client pentru a specifica destinaţia mesajelor pe care le produce şi sursa mesajelor pe care le consumă. În mecanismul de comunicare punct la punct, destinaţiile sunt denumite cozi. Pentru modul de comunicare publicare-abonare, destinaţiile poartă numele de teme. O aplicaţie JMS poate folosi mai multe cozi, mai multe teme sau obiecte din ambele categorii.

Pentru a crea o destinaţie, trebuie specificată o resursă JMS care specifică numele JNDI pentru destinaţie. Fiecare resursă de tip acest tip referă o anumită locaţie fizică.

Un administrator poate să creeze o locaţie fizică în mod explicit, însă dacă nu o va face, serverul de aplicaţii va realiza această operaţie atunci când este necesar, ştergând resursa dacă destinaţia este înlăturată.

Pe lângă injectarea unei fabrici de conexiuni în aplicaţia client, de regulă se injectează şi o resursă de tip destinaţie. Spre diferenţă însă de fabricile de conexiuni, destinaţiile sunt specifice modelului de comunicare punct la punct sau publicare-abonare. Pentru a crea o aplicaţie care permite folosirea aceluiaşi cod sursă atât pentru cozi cât şi pentru teme, se va folosi un obiect de tipul (generic) Destination.

queue = (Queue) initialContext.lookup(Constants.QUEUE_NAME);
topic = (Topic) initialContext.lookup(Constants.TOPIC_NAME);

În cazul unei aplicaţii Java EE, obiectele administrate sun de obicei plasate în subcontextul de nume jms. Injectarea resurselor în acest caz se face tot prin intermediul adnotării @Resource, folosind atributul lookup:

@Resource(lookup = "jms/MyQueue")
private static Queue queue;
@Resource(lookup = "jms/MyTopic")
private static Topic topic;

În cadrul interfeţelor de bază, se pot combina diferitele fabrici de conexiuni cu tipuri de destinaţii diferite. Astfel, o resursă de tip QueueConnectionFactory se poate folosi în corelaţie cu un obiect Topic în timp ce o resursă de tip TopicConnectionFactory poate fi utilizată împreună cu un obiect Queue.

O conexiune încapsulează o conexiune virtuală către un furnizor JMS de servicii de mesagerie.

Procesul de descoperire a resurselor definite în contextul JNDI se face prin intermediul protocolului IIOP (comunicaţia având loc pe portul 3700) în timp ce transferul propriu-zis al mesajelor se face printr-un protocol specific JMS, pe portul 7676.

O conexiune poate fi utilizată pentru a crea una sau mai multe sesiuni. De regulă, o conexiune este creată printr-un obiect JMSContext, dar se pot utiliza şi obiecte Connection, QueueConnection, TopicConnection.

QueueConnection queueConnection = queueConnectionFactory.createQueueConnection();
TopicConnection topicConnection = topicConnectionFactory.createTopicConnection();

În cazul platformei Java EE, posibilitatea de a crea mai multe sesiuni din cadrul unei singure conexiuni este limitată doar pentru aplicaţiile client. În cazul aplicaţiilor de tip enterprise şi web, o conexiune nu poate crea mai mult de o sesiune.

O sesiune reprezintă un context ce rulează într-un singur fir de execuţie pentru producerea şi consumarea de mesaje. Deşi sesiunea (împreună cu conexiunea) sunt create printr-un obiect JMSContext, se pot utiliza şi obiecte Session, QueueSession şi TopicSession.

QueueSession queueSession = 
             queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
TopicSession topicSession = 
             topicConnection.createTopicSession(false, Session.AUTO_ACKNOWLEDGE);

Sesiunile sunt utilizate spre a crea producători şi consumatori de mesage, mesaje, obiecte pentru consultarea (eng. browsing) cozilor şi destinaţiilor temporare. Sesiunile serializează execuţia ascultătorilor de mesaje. O sesiune oferă un context tranzacţional cu care grupează un set de operaţii de transmitere şi de primire într-o unitate atomică.

Un obiect JMSContext asociază o conexiune şi o sesiune într-un singur obiect, oferind atât o conexiune activă către furnizorul JMS de servicii de mesagerie cât şi un context ce rulează într-un singur fir de execuţie pentru operaţii de transmitere şi primire de mesaje. Din acest punct de vedere, funcţionalitatea sa este asemănătoare cu a unui obiect de tip sesiune.

Un astfel de obiect se creează de obicei într-un bloc try-with-resources, printr-un apel al metodei createContext pe un obiect de tip fabrică de conexiuni:

try (JMSContext context = connectionFactory.createContext()) {
   // ...
}

În acest fel, contextul nu trebuie închis explicit, întrucât acest lucru se produce la sfârşitul blocului try-with-resources, în caz contrar fiind necesar să se apeleze metoda close() pentru a închide conexiunea dacă aplicaţia şi-a terminat sarcinile. Trebuie să se asigure faptul că toate operaţiile din cadrul blocului try-with-resources sunt executate.

Dacă este apelată fără argumente din contextul unei aplicaţii client sau dintr-un client Java SE precum şi din cadrul unui container Java EE EJB / web în care nu există tranzacţii JTA în progres la momentul respectiv de timp, metoda createContext() realizează o sesiune fără tranzacţii în care confirmarea pentru primirea mesajelor se face automat (JMSContext.AUTO_ACKNOWLEDGE). Alternativ, se poate specifica un mesaj de confirmare al mesajelor specific în care clientul transmite în mod explicit confirmare (JMSContext.CLIENT_ACKNOWLEDGE), respectiv în care sesiunea transmite întârziat confirmările (JMSContext.DUPS_ON_ACKNOWLEDGE).

Apelată dintr-un container Java EE în care există tranzacţii JTA în progres la momentul de timp respectiv, va determina crearea unei sesiuni cu tranzacţii. Acest tip de comportament poate fi obţinut în cadrul aplicaţiilor client sau al clienţilor Java SE prin precizarea explicită a parametrului JMSContext.SESSION_TRANSACTED. Sesiunea foloseşte întotdeauna tranzacţii locale.

Un producător de mesaje este un obiect creat prin intermediul unui obiect JMSContext sau a unei sesiuni şi utilizat pentru transmiterea de mesaje către o destinaţie. Un astfel de obiect implementează interfaţa JMSProducer şi este creat printr-un apel al metodei createProducer().

try (JMSContext context = connectionFactory.createContext()) {
    JMSProducer producer = context.createProducer();
    // ...
}

Întrucât obiectul JMSProducer consumă destul de puţine resurse, nu este necesar ca acesta să fie reţinut într-un obiect distinct, putând fi creat de fiecare dată când este transmis un mesaj, prin intermediul metodei send().

context.createProducer().send(destination, message);

Un consumator de mesaje este un obiect creat prin intermediul unui obiect JMSContext sau a unei sesiuni şi utilizat pentru primirea de mesaje de la o destinaţie. Un astfel de obiect implementează interfaţa JMSConsumer şi este creat printr-un apel al metodei createConsumer():

try (JMSContext context = connectionFactory.createContext()) {
    JMSConsumer consumer = context.createConsumer(destination);
    // ...
}

Un consumator de mesaje permite unui client JMS să îşi exprime interesul vis-a-vis de un furnizor de servicii de mesagerie, acesta fiind responsabil de livrarea mesajelor de la destinaţie la consumatorii asociaţi acesteia. Dacă la construirea unui consumator de mesaje se foloseşte un obiect JMSContext, livrarea de mesaje începe imediat, un astfel de comportament putând fi dezactivat prin apelul metodei setAutoStart(false) (atunci când se creează obiectul JMSContext) şi apoi apelând metoda start() explicit pentru a începe procesul de livrare al mesajelor, respectiv stop() pentru a-l întrerupe temporar.

Metoda receive() este utilizată pentru a consuma mesajele în mod sincron, aceasta putând fi apelată la orice moment după construirea unui consumator. Dacă nu se indică nici un argument sau argumentul este 0, metoda se blochează indefinit până când e primit un mesaj (consumer.receive() = consumer.receive(0)). Metoda poate fi apelată cu un argument ce indică timpul de aşteptare, după care aceasta se va întoarce (cu un rezultat null) chiar şi în situaţia în care mesajul nu a fost primit.

Message message = consumer.receive(Constants.TIME_OUT);

Dacă se doreşte ca livrarea de mesaje să se desfăşoare asincron dintr-o aplicaţie client sau dintr-un client Java SE, trebuie folosit un ascultător de mesaje, adică un obiect care funcţionează ca un proces de gestiune al evenimentelor de primire a mesajelor. Acesta implementează interfaţa MessageListener, care conţine o singură metodă, onMessage(), în care se definesc operaţiile ce trebuie realizate la livrarea unui mesaj. Asocierea dintre un consumator de mesaje şi un ascultător de mesaje se face prin metoda setMessageListener(). Furnizorul JMS apelează în mod automat onMessage() de fiecare dată când este primit un mesaj. Aceasta primeşte un argument de tip Message care poate fi convertit la alt tip de mesaj, în cazul în care este necesar. În cazul containerelor Java EE EJB / web se folosesc componente orientate pe mesaje pentru livrarea asincronă (o componentă orientată pe mesaje implementează de asemenea interfaţa MessageListener şi conţine metoda onMessage()). Metoda onMessage() ar trebui să gestioneze orice fel de excepţii. Generarea unei excepţii de tip RuntimeException este considerată eroare de programare.

În situaţia în care aplicaţia are nevoie de filtrarea mesajelor pe măsură ce acestea sunt primite, se poate folosi un selector de mesaje JMS, care permite consumatorului de mesaje pentru o destinaţie să indice prin ce se caracterizează mesajele care îl interesează. Astfel, sarcina de filtrare a mesajelor este transferată furnizorului de gestiune al mesajelor, în detrimentul aplicaţiei. Un selector de mesaje este un obiect java.lang.String care conţine o expresie bazată pe un subset al sintaxei SQL92 pentru expresii condiţionale. Metoda createConsumer() şi cele derivate din aceasta permit specificarea unui selector de mesaje ca argument când se construieşte un consumator de mesaje. Astfel, consumatorul de mesaje va primi doar mesajele ale căror antete şi proprietăţi se potrivesc cu cele specificate în selectorul de mesaje.

Un selector de mesaje nu poate selecta mesajele pe baza conţinutului acestora.
JMSConsumer consumer = session.createConsumer(
                           queue, 
                           Constants.USER_NAME_PROPERTY + "= '" + userName + "'
                       );

Semantica consumării mesajelor din cadrul unei teme este mai complexă decât în cazul unei cozi. O aplicaţie consumă mesajele dintr-o temă prin crearea unui abonament în cadrul acesteia, creând un consumator asociat acestuia. Abonamentele pot fi tranzitive sau durabile şi pot fi partajate sau nepartajate. Un abonament poate fi gândit ca o entitate în cadrul furnizorului JMS în timp ce consumatorul este un obiect în cadrul aplicaţiei.

O subscripţie va primi o copie a fiecărui mesaj transmis în cadrul temei după ce abonamentul este creat, cu excepţia cazului în care a fost specificat un selector de mesaje. În această situație, doar mesajele ale căror proprietăţi întrunesc condiţiile specificate vor fi primite de subscripţie.

Unele abonamente sunt limitate la un singur consumator. În acest caz, toate mesajele din cadrul subscripţiei sunt livrate către acesta. Alte subscripţii permit mai mulţi consumatori. Într-o astfel de situaţie, fiecare mesaj primit de către aceasta va fi livrat către un singur consumator.

JMS nu defineşte mecanismul prin care mesajele sunt distribuite între mai mulţi consumatori din cadrul aceluiaşi abonament.

Subscripţiile pot fi tranzitive sau durabile.

Un abonament tranzitiv există atâta vreme cât există un consumator activ în cadrul său. Astfel, orice mesaj transmis către temă va fi adăugat la subscripţie doar dacă un consumator există şi nu este închis. Un abonament tranzitiv poate fi nepartajat sau partajat.

O subscripţie tranzitivă nepartajată nu are un nume şi poate avea asociat doar un singur obiect consumator. Este creată automat atunci când se construieşte obiectul consumator. Nu este persistentă, fiind ştearsă automat dacă obiectul consumator este închis. Metoda JMSContext.createConsumer() creează un consumator de mesaje pe o subscripţie tranzitivă nepartajată dacă destinaţia este o temă.

O subscripţie tranzitivă partajată este identificată printr-o denumire şi un identificator al clientului (opţional), putând avea asociate mai multe obiecte de tip consumator. Este creată automat atunci când este construit primul obiect consumator. Nu este persistentă, fiind ştearsă automat dacă şi ultimul obiect consumator este închis.

Deşi implică costuri mai mari, un abonament poate fi durabil, acesta fiind persistent şi continuând să acumuleze mesaje până la momentul în care este şters explicit, chiar dacă nu mai există obiecte de tip consumator care să preia mesaje din cadrul său. O astfel de abordare este necesară atunci când este necesar să se asigure că aplicaţia va primi toate mesajele transmise.

Ca şi în cazul subscripţiilor tranzitive, un abonament durabil poate fi nepartajat sau partajat.

Un abonament durabil nepartajat este caracterizat printr-o denumire şi un identificator de client (ce trebuie specificat) putând avea un singur consumator asociat cu el.

Un abonament durabil partajat se caracterizează printr-o denumire şi un identificator de client (opţional), putând avea asociate mai multe obiecte de tip consumator de mesaje.

O subscripţie durabilă care există dar nu are asociat nici un obiect de tip consumator de mesaje deschis este considerată inactivă.

Metoda JMSContext.createDurableConsumer() poate fi folosită pentru construirea unui consumator de mesaje asociat unui abonament durabil nepartajat. O astfel de subscripţie poate avea doar un singur consumator la un moment dat. Un consumator identifică abonamentul durabil din care va primi mesaje prin specificarea unei valori unice reţinute de furnizorul JMS. Următoarele obiecte de tip consumator de mesaje ce utilizează acelaşi identificator unic reiau subscripţia din starea în care a fost lăsată anterior. Dacă un abonament durabil nu are nici un consumator activ, furnizorul JMS reţine mesaje corespunzătoare până când sunt primite de un consumator sau până când expiră. Identitatea unei subscripţii durabile poate fi determinată fie prin specificarea unei valori unice pentru întreaga conexiune (acesta putând fi stabilit administrativ pentru o fabrică de conexiuni specifică unui client) sau prin indicarea unei teme şi a unei denumiri pentru abonamentul respectiv. Dacă un consumator nu mai este necesar, acesta va fi închis prin apelarea metodei close():

JMSConsumer consumer = context.createDurableConsumer(topic, Constants.SUBSCRIPTION_NAME);
// ...
consumer.close();

Furnizorul JMS stochează mesajele transmise către temă în acelaşi mod în care procedează şi în cazul unei cozi. Dacă o altă aplicaţie va apela metoda createDurableConsumer() folosind aceeaşi fabrică de conexiuni şi acelaşi identificator pentru client, aceeaşi temă şi aceeaşi denumire a subscripţiei, atunci subscripţia este reactivată şi furnizorul JMS livrează mesajele care au fost transmise pe perioada în care subscripţia a fost inactivă.

Pentru a şterge o subscripţie durabilă, trebuie închişi toţi consumatorii de mesaje, după care trebuie apelată metoda unsubscribe() primind ca parametru numele său, aceasta ştergând inclusiv starea pe care furnizorul JMS o menţine.

context.unsubscribe(Constants.SUBSCRIPTION_NAME);

O subscripţie durabilă partajată permite asocierea mai multor consumatori de mesaje. În acest caz, fabrica de conexiuni utilizată nu trebuie să aibă un identificator al clientului.

Un abonament asociat unei teme creat de metodele createConsumer() sau createDurableConsumer() poate avea un singur consumator (deşi o temă poate avea mai multe obiecte de acest tip). Astfel, mai mulţi clienţi care consumă din cadrul aceleiaşi teme au, prin definiţie, mai multe subscripţii asociate şi toţi clienţii primesc toate mesajele transmise către tema respectivă. O excepţie de la acest caz este situaţia în care sunt implementate selectoare de mesaje.

Totuşi, există posibilitatea de a crea o subscripţie tranzitivă partajată pentru o temă folosind metoda createSharedConsumer() şi specificând nu doar destinaţia (denumirea temei) ci şi a abonamentului în sine. Prin intermediul unei subscripţii partajate, mesajele vor fi distribuite între mai mulţi clienţi care folosesc aceeaşi temă şi aceeaşi denumire a subscripţiei. Fiecare mesaj va fi adăugat fiecărei subscripţii, dar fiecare mesaj de acest tip va fi livrat către un singur consumator din cadrul unei subscripţii, deci va fi primit doar de către un singur client, comportamentul fiind util dacă se doreşte partajarea încărcării. Această funcţionalitate îmbunătăţeşte scalabilitatea aplicaţiilor client, atât pentru Java SE cât mai ales în cazul Java EE. Componentele orientate pe mesaje împart sarcina de a procesa mesajele din cadrul unei teme pe mai multe fire de execuţie.

De asemenea, pot fi create abonamente durabile partajate. Spre a construi un astfel de obiect, se apelează metoda JMSContext.createSharedDurableConsumer() specificând denumirea temei, respectiv a subscripţiei.

Mesajele transmise unei cozi rămân în cadrul acesteia până când consumatorul de mesaje le primeşte. API-ul JMS implementează un obiect QueueBrowser ce permite consultarea mesajelor din coadă şi afişarea valorilor conţinute de antetele acestora. În acest scop, se foloseşte metoda createBrowser() pe un obiect JMSContext:

QueueBrowser browser = context.createBrowser(queue);
Metoda createBrowser() suportă şi un alt argument, indicând un selector de mesaje.
Specificaţia JMS NU oferă posibilitatea de a consulta o temă, deoarece mesajele sunt şterse de obicei din cadrul acesteia de către furnizor imediat ce sunt adăugate, chiar dacă nu există consumatori de mesaje care să realizeze acest lucru. Cu toate că subscripţiile durabile permit mesajelor să rămână în cadrul unei teme în timp ce consumatorul de mesaje nu este activ, specificaţia JMS nu defineşte nici o facilitate pentru examinarea acestora.

În tratarea excepţiilor trebuie să se aibă în vedere faptul că rădăcina tuturor excepţiilor ce pot fi prinse este JMSException în timp ce rădăcina pentru toate excepţiile ce nu pot fi prinse este JMSRuntimeException.

Mulţi utilizatori recurg la JMS deoarece aplicaţiile pe care le dezvoltă nu pot gestiona situaţii precum mesaje pierdute sau mesaje duplicate, fiind necesar ca fiecare mesaj să fie primit o singură dată, funcţionalitate oferită de distribuţiile dezvoltate de diferiţi producători.

Cel mai sigur mod de a produce un mesaj este folosirea tipului PERSISTENT şi transferul său în cadrul unei tranzacţii. Tranzacţiile permit ca mai multe mesaje să fie transmise sau să fie primite într-o singură operaţie atomică. Astfel, tranzacţia este o unitate de lucru în care pot fi grupate o serie de operaţii, cum ar fi transmiterea şi primirea de mesaje, astfel că acestea fie reuşesc toate, fie eşuează împreună. Cel mai sigur mod de consumare a mesajelor este în cadrul unei tranzacţii, fie dintr-o coadă, fie dintr-o temă din cadrul unei subscripţii durabile.

Mesajele JMS au tipul PERSISTENT în mod implicit; astfel de mesaje nu vor fi pierdute în cazul producerii unei erori la nivelul furnizorului JMS.
În cazul platformei Java EE, tranzacţiile permit cuplarea operaţiilor de transmitere, respectiv primire a mesajelor cu citiri şi scrieri din şi în baza de date în mod atomic.

Unele funcţionalităţi permit unei aplicaţii să-şi îmbunătăţească performanţele. De exemplu, pentru mesajele se pot indica timpi de expirare după o anumită perioadă de timp, astfel încât consumatorii să nu primească informaţii care nu mai au actualitate. Totodată, mesajele pot fi transmise asincron, iar confirmarea de primire se poate realiza în mai multe moduri. Alte funcţionalităţi nu sunt legate de siguranţă, dar se pot dovedi utile în anumite circumstanţe. Astfel, se pot crea destinaţii temporare ce durează doar pe parcursul conexiunii în care au fost construite.

Funcționalități Avansate JMS

Prin intermediul funcționalităților avansate JMS, se asigură un nivel de încredere și performanțele solicitate de cele mai multe aplicații care folosesc un sistem de mesagerie: livrarea unui mesaj cu certitudine o singură dată (evitându-se astfel situațiile în care mesajul este pierdut sau este transmis de mai multe ori). Mecanismul prin care poate fi obținut un astfel de comportament este folosirea unui mesaj de tip PERSISTENT (implicit), care nu poate fi pierdut chiar în situația în care se produce o eroare la nivelul furnizorului de servicii de mesagerie (serverul de aplicații) și utilizarea unei tranzacții, unitate de lucru în care pot fi grupate mai multe operații (de tip trimitere / primire de mesaje) executate atomic (astfel încât fie toate sunt rulate cu succes, fie eșuează împreună).

Totodată, funcționalitățile avansate JMS permit îmbunătățirea performanței:

  • specificarea unui timp de expirare al mesajelor face să nu fie transmise mesaje al căror conținut nu mai este de actualitate;
  • transmiterea de mesaje în mod asincron;
  • indicarea unor nivele pentru confirmarea transmiterii mesajelor;
  • implementarea unor destinații temporare (care există doar în contextul conexiunii în care au fost create).

Controlul procesului de confirmare a transmiterii mesajelor

Un mesaj JMS nu este considerat ca fiind transmis cu succes până ce nu primește o confirmare explicită. Procesul de confirmare poate fi inițiat (ulterior livrării mesajului) fie de furnizorul de servicii de mesagerie, fie de componenta căreia i-a fost transmis, în funcție de mecanismul de confirmare utilizat.

a) În sesiunile tranzacționate local, un mesaj este confirmat atunci când sesiunea este consemnată (eng. committed). Dacă se produce o revenire la nivelul tranzacției, toate mesajele sunt transmise din nou.

b) Într-o tranzacție JTA (într-un container Java EE sau componentă Java Beans), un mesaj este confirmat atunci când tranzacția este consemnată.

c) În sesiune netranzacționate, confirmarea unui mesaj și momentul la care se produce acest lucru sunt controlate prin intermediul valorii pe care o ia parametrul metodei createContext():

  • JMSContext.AUTO_ACKNOWLEGDE (implicită pentru clienți Java SE): contextul JMSContext confirmă în mod automat transmiterea mesajului, fie în momentul în care se termină metoda receive(), fie când se termină metoda onMessage() a obiectului de tip MessageListener care a fost invocat pentru procesarea mesajului.
O livrare sincronă a unui mesaj folosind un obiect JMSContext care folosește confirmarea automată implică faptul că trimiterea sa se realizează concomitent cu primirea confirmării, după care este realizată procesarea sa.
  • JMSContext.CLIENT_ACKNOWLEDGE: clientul confirmă transmiterea prin apelul metodei acknowledge() pe obiectul mesaj; astfel, procesul de confirmare se realizează la nivelul sesiunii (confirmarea unui mesaj implică în mod automat confirmarea transmiterii tuturor mesajelor care au fost livrate în cadrul respectivei sesiuni, indiferent de obiectul pentru care este invocată metoda respectivă);
Pentru platforma Java EE, o astfel de valoare poate fi folosită doar într-un client de tip aplicație, nu însă și pentru componente web sau de tip Java Bean.
  • JMSContext.DUPS_OK_ACKNOWLEDGE: contextul JMSContext realizează procesul de confirmare cu întârziere, ceea ce poate determina transmiterea unei mesaje de mai multe ori, în cazul în care la nivelul furnizorului de servicii de mesageriese produc unele erori de funcționare; o astfel de opțiune poate fi folosită numai în cazul în care clientul poate gestiona situații de acest tip (atunci când un mesaj este retransmis, furnizorul de servicii de mesagerie va specifica acest lucru prin valoarea true a proprietății JMSRedelivered a antetului său).
Acestă opțiune reduce supraîncărcarea de la nivelul sesiunii, eficientizând operațiile pe care aceasta trebuia să le realizeze pentru a preveni situația în care erau transmise duplicate ale unui mesaj.

Dacă mesajele au fost preluate din cadrul unei cozi și nu au fost confirmate la momentul în care obiectul JMSContext este închis, furnizorul serviciului de mesagerie le reține, transmițându-le din nou când o componentă accesează destinația respectivă. Același comportament poate fi observat și în cazul unor abonamente durabile. Din acest motiv, atunci când se folosește o destinație de tip coadă sau un abonament durabil, trebuie utilizată metoda JMSContext.recover() pentru a se opri un obiect JMSContext netranzacționat, repornindu-l cu mesajele care nu au fost confirmate. Efectul acestei metode constă în plasarea indicatorului din cadrul fluxului de mesaje livrate spre a referi ultimul mesaj care a fost confirmat. Totuși, mesajele care sunt transmise pot fi diferite de cele care au fost transmise inițial, dacă acestea au expirat sau există mesaje cu prioritate mai mare.

În cazul unon abonamente tranzitive, furnizorul de servicii de mesagerie poate ignora mesajele care nu au fost confirmate atunci când este apelată metoda JMSContext.recover().

Parametrii de configurare pentru transmiterea mesajelor

În momentul în care este transmis un mesaj folosind sistemul Java Message Service, pot fi configurați diverși parametri, care indică:

  • dacă acestea sunt persistente (dacă sunt pierdute sau nu în cazul în care se produce o eroare la nivelul furnizorului de servicii JMS);
  • nivelul de prioritate, cu un impact asupra ordinii în care acestea sunt livrate;
  • perioada de timp după care acestea expiră, implicând faptul că acestea nu vor mai fi livrate, în cazul în care nu mai sunt de actualitate;
  • întârzierea cu care vor fi livrate.
Înlănțuirea metodelor permite agregarea mai multor opțiuni în momentul în care este creat un producător de mesaje pentru care este apelată metoda send().

Specificarea persistenței mesajelor

API-ul JMS implementează două mecanisme de livrare, indicând comportamentul pe care îl are furnizorul de servicii de mesagerie în cazul producerii unei erori. Aceste comportamente sunt definite prin intermediul unor câmpuri din interfața DeliveryMode:

  • PERSISTENT (implicit): indică faptul că nu este permisă pierderea mesajelor în cazul producerii unor erori, astfel că acestea sunt stocate (prin intermediul unor jurnale) pe un dispozitiv dedicat atunci când este transmis;
  • NON_PERSISTENT: nu oferă nici un fel de garanţie că mesajul nu este pierdut în situaţia în care furnizorul de servicii de mesagerie se defectează.

Pentru a indica modul de transmitere a mesajelor, se folosește metoda setDeliveryMode() din cadrul interfeței JMSProducer, comportamentul respectiv aplicându-se pentru toate mesajele transmise de producătorul respectiv.

jmsContext.createProducer().setDeliveryMode(DeliveryMode.NON_PERSISTENT).send(destination, message);
În cazul în care nu se specifică nici un mecanism de livrare a mesajelor, se va considera că acesta este PERSISTENT.
Folosirea modului de transmitere NON_PERSISTENT poate îmbunătăți performanțele, reducând supraîncărcarea spațiului de stocare, acesta putând fi utilizat numai în situația în care clientul poate opera și dacă se pierd unele mesaje.

Specificarea unor nivele de prioritate a mesajelor

Utilizatorul poate indica nivele de prioritate a mesajelor pentru a furnizorul de servicii de mesagerie să livreze mesajele urgente mai repede.

În acest scop, trebuie folosită metoda setPriority() din cadrul interfeței JMSProducer, nivelul de prioritate indicat (un număr întreg cuprins între 0 și 9) fiind folosit pentru toate mesajele transmise prin intermediul producătorului respectiv.

jmsContext.createProducer().setPriority(Constants.MESSAGE_PRIORITY).send(destination, message);
În cazul în care nu se specifică nici un nivel de prioritate, se va considera că acesta este 4.
Un furnizor de servicii de mesagerie care implementează specificația JMS va încerca să livreze mesajele cu prioritate mai mare înaintea mesajelor cu prioritate mai mică, însă nu există nici o garanție că mesajele sunt transmise exact în ordinea nivelului de prioritate specificat.

Specificarea unui timp de expirare a mesajelor

Implicit, un mesaj nu expiră niciodată.

În situația în care conținutul unui mesaj nu mai este de actualitate după o anumită perioadă de timp, poate fi indicat un timp de expirare. Acest comportament poate fi folosit pentru mesajele a căror semnificație este strâns legată de momentul la care sunt transmise, pierzându-și valoarea după un anumit interval (mai mare sau mai mic).

În acest scop, trebuie folosită metoda setTimeToLive() din cadrul interfeței JMSProducer, perioada de timp exprimată în milisecunde fiind folosită pentru toate mesajele transmise prin intermediul producătorului respectiv.

jmsContext.createProducer().setTimeToLive(Constants.MESSAGE_EXPIRY_PERIOD).send(destination, message);
Atunci când mesajul este trimis, atributul timeToLive este adăugat la momentul de timp curent pentru a se obține termenul la care acesta va expira. Orice mesaj care nu este livrat în această fereastră temporală este distrus, conservându-se astfel spațiul de stocare și resursele de procesare.
Dacă valoarea specificată pentru timpul de expirare este 0, mesajul nu va expira niciodată.

Specificarea întârzierii cu care vor fi livrate mesajele

Dacă se dorește, se poate indica un interval de timp care trebuie să se scurgă până la momentul în care furnizorul de servicii de mesagerie livrează mesajele.

În acest scop trebuie folosită metoda setDeliveryDelay() din cadrul interfeței JMSProducer, perioada de timp exprimată în milisecunde fiind folosită pentru toate mesajele transmise prin intermediul producătorului respectiv.

jmsContext.createProducer().setDeliveryDelay(Constants.MESSAGE_DELIVERY_DELAY).send(destination, message);

Întrucât metodele prin intermediul cărora sunt precizate valorile parametrilor de configurare a mesajelor întorc rezultate de tipul JMSProducer, specificarea de valori pentru atribute diferite poate fi realizată în cadrul unei singure instrucțiuni, prin înlănțuire:

jmsContext.createProducer()
          .setDeliveryMode(DeliveryMode.NON_PERSISTENT)
          .setPriority(Constants.MESSAGE_PRIORITY)
          .setTimetoLive(Constants.MESSAGE_EXPIRY_PERIOD)
          .setProperty("userAttribute", "userValue")
          .setDeliveryDelay(Constants.MESSAGE_DELIVERY_DELAY)
          .send(destination, message);

Așa cum se poate observa, interfața JMSProducer permite specificarea unor valori pentru atribute definite de utilizator.

Toți acești parametrii de configurare pot fi precizați pentru un obiect de tip mesaj, în cazul în care se dorește ca fiecare mesaj transmis de producător (prin metoda send()) să fie caracterizat prin propriile valori ale acestora.

Crearea unor destinații temporare

De regulă, destinațiile (cozi și teme) sunt obiecte administrative care sunt create prin intermediul utilitarelor puse la dispoziție de furnizorul de servicii de mesagerie. Fiind persistente, acestea nu sunt instanțiate în cadrul aplicațiilor care le utilizează, ci obținute ca referințe prin injectarea în codul sursă.

API-ul JMS permite însă crearea de destinații temporare (obiecte de tipul TemporaryQueue și TemporaryTopic) - în mod dinamic - care există numai pe durata conexiunii din care fac parte.

În acest scop, trebuie folosite metodele createTemporaryQueue(), respectiv createTemporaryTopic() din cadrul obiectului JMSContext:

TemporaryQueue temporaryQueue = jmsContext.createTemporaryQueue();
TemporaryTopic temporaryTopic = jmsContext.createTemporaryTopic();

Orice producător de mesaje poate trimite mesaje către destinația temporară.

Un consumator de mesaje poate primi mesaje de la destinația temporară numai în cazul în care a fost obținut folosind aceeași conexiune care a creat-o.

În cazul în care se închide conexiunea din care face parte o destinație temporară, aceasta este distrusă și conținutul său este pierdut definitiv.

Mecanismele de tip cerere/răspuns reprezintă situații pentru care este adecvată implementarea unor destinații temporare:

  • un producător de mesaje va trimite o cerere, indicând destinația temporară în atributul JMSReplyTo din cadrul antetului mesajului ca fiind locația la care trebuie primit răspunsul;
  • un consumator de mesaje poate referi mesajul original, folosind atributul JMSCorrelationID din cadrul antetului mesajului ca având valoarea JMSMessageID a acestuia;
TemporaryTopic temporaryTopic = jmsContext.createTemporaryTopic();
Message requestMessage = jmsContext.createTextMessage("This is a request message");
requestMessage.setJMSReplyTo(temporaryTopic);
jmsContext.createProducer().send(temporaryTopic, requestMessage);
MessageAnalyzer.java
public class MessageAnalyzer implements MessageListener {
 
    @Override
    public void onMessage(Message requestMessage) {
        Message replyMessage = jmsContext.createTextMessage("This is a reply message");
        replyMessage.setJMSCorrelationID(requestMessage.getJMSMessageID());
        jmsContext.createProducer().send((Topic)requestMessage.getJMSReplyTo(), replyMessage);
    }
 
    // ...
 
}

Utilizarea de tranzacții locale JMS

O tranzacție grupează o serie de operații într-o unitate de lucru atomică.

  • în contextul în care o acțiune (dintre acestea) eșuează, tranzacția revine la starea anterioară, operațiile putând fi reluate din acest punct;
  • dacă toate operațiile sunt executate cu succes, tranzacția poate fi consemnată.

Într-un client Java SE, tranzacțiile locale pot fi folosite pentru a grupa trimiteri și primiri de mesaje.

Metoda commit() a unui obiect de tip JMSContext va fi utilizată pentru a consemna o tranzacție.

  • în cazul mai multor operații de trimitere a unor mesaje în cadrul aceleiași tranzacții, acestea nu vor fi transferate la nivelul destinației până când tranzacția nu este consemnată;
  • în cazul mai multor operații de primire a unor mesaje în cadrul aceleiași tranzacții, ele nu vor fi confirmate până când tranzacția nu este consemnată.

Metoda rollback() a unui obiect de tip JMSContext va fi folosită pentru a se reveni la starea anterioară unei tranzacții.

  • în cazul mai multor operații de trimitere a unor mesaje în cadrul aceleiași tranzacții, toate mesajele produse vor fi distruse;
  • în cazul mai multor operații de primire a unor mesaje în cadrul aceleiași tranzacții, ele vor fi recuperate și livrate din nou (cu excepția cazului în care au expirat).

O sesiune tranzacționată este întotdeauna asociată unei tranzacții, aceasta putând fi obținută prin intermediul metodei createContext() invocată pe o fabrică de conexiuni, primind parametrul JMSContext.SESSION_TRANSACTED:

JMSContext jmsContext = connectionFactory.createContext(JMSContext.SESSION_TRANSACTED);

Odată cu apelarea uneia dintre metodele commit() sau rollback(), se încheie tranzacția curentă.

Închiderea unei sesiuni tranzacționate implică revenirea la starea anterioară tranzacției în curs (inclusiv trimiteri / primiri de mesaje) - echivalentul unui apel al metodei rollback().

În cadrul platformei Java EE (fie că este vorba de o aplicație web sau de o componentă Java Beans) nu pot fi folosite tranzacții locale, ci tranzacții JTA.

În cadrul unei tranzacții locale, pot fi agregate mai multe operații de trimitere / primire mesaje, cu condiția ca acestea să fie realizate folosind același obiect de tip JMSContext.

În implementarea unui mecanism de tip cerere / răspuns NU trebuie folosite tranzacții locale, întrucât o astfel de încercare va genera blocarea aplicației, de vreme ce metoda send() nu va fi realizată până ce nu se realizează consemnarea tranzacției. Totodată, metoda receive() nu poate primi mesajul atâta vreme cât acesta nu a fost trimis.
// DEADLOCK !!!
Message messageToSend = jmsContext.createTextMessage();
messageToSend.setJMSReplyTo(queue);
jmsContext.createProducer().send(queue, messageToSend);
consumer = jmsContext.createConsumer(queue);
Message messageReceived = consumer.receive();
jmsContext.commit();

Astfel, producerea și consumarea unui același mesaj nu pot fi incluse în cadrul aceleiași tranzacții.

Tranzacțiile implică operații care sunt realizate între clienți și furnizorul de servicii de mesagerie, care intervine între procesul de producere și de consumare de mesaje. O tranzacție reprezintă un contract între aceste entități, definind dacă un mesaj este transmis la o destinație sau este primit de la o destinație. O tranzacție NU poate implementa un contract între doi clienți (unul care trimite și unul care primește).

Trimiterea unuia sau mai multor mesaje de către un client către una sau mai multe destinații poate forma o tranzacție, de vreme ce implică o singură interacțiune cu furnizorul de servicii de mesagerie folosind un singur obiect de tip JMSContext.

Primirea unuia sau mai multor mesaje de către un client de la una sau mai multe destinații poate forma o tranzacție, de vreme ce implică o singură interacțiune cu furnizorul de servicii de mesagerie folosind un singur obiect de tip JMSContext.

Între doi clienți care nu interacționează direct (nu folosesc același obiect de tip JMSContext) nu se poate realiza o tranzacție.

Aceasta este distincția dintre mesagerie și procesare sincronă. În loc de să realizeze o cuplare strânsă între destinatarul și expeditorul unui mesaj, JMS asociază expeditorul cu obiectul administrat și separat destinatarul cu obiectul administrat. Astfel, deși atât expeditorul cât și destinatarul sunt ambele strâns cuplate cu furnizorul de servicii de mesagerie, ele sunt slab cuplate între ele.

La crearea unui obiect de tip JMSContext poate fi specificat dacă sesiunea asociată este tranzacționată sau nu, prin includerea sau nu a parametrului JMSContext.SESSION_TRANSACTED:

try (JMSContext jmsContext = connectionFactory.createContext(JMSContext.SESSION_TRANSACTED)) {
    // ...
} catch (Exception exception) {
    System.out.println("An exception has occurred: " + exception.getMessage());
    if (Constants.DEBUG)
        exception.printStackTrace();
}

Metodele commit() și rollback() sunt asociate sesiunii din cadrul obiectului JMSContext. Într-o singură tranzacție locală pot fi realizate mai multe cozi și teme sau pe orice combinație a acestora.

Transmiterea asincronă de mesaje

De regulă, atunci când se transmite un mesaj persistent, metoda send() se blochează până la momentul în care furnizorul de servicii de mesagerie confirmă faptul că mesajul a fost livrat cu succes.

Mecanismul de transmitere asincronă de mesaje permite ca aplicația să realizeze această operație, continuându-și funcționarea concomitent cu așteptarea rezultatului metodei send().

O astfel de funcționalitate este disponibilă numai în cadrul clienților Java SE.

Implementarea unui mecanism de transmitere asincronă a unor mesaje presupune furnizarea unui obiect cu apel invers (eng. callback) de tip CompletionListener pentru care trebuie specificată cel puțin metoda onCompletion(), apelată în mod automat atunci când operația send() a fost realizată cu succes.

Opțional, poate fi dezvoltată și metoda onException(), care va fi invocată în mod automat dacă operația send() eșuează.
SendMethodListener.java
public class SendMethodListener implements CompletionListener {
 
    @Override
    public void onCompletion(Message message) {
        System.out.println("Message " + message + " sent successfully");
    }
 
    @Override
    public void onException(Message message, Exception exception) {
        System.out.println("Message " + message + " not sent successfully: " + exception.getMessage());
        if (Constants.DEBUG)
            exception.printStackTrace();
    }
}

O implementare a acestei clase trebuie furnizată ca parametru al metodei setAsync() apelată în producătorul de mesaje (obiect de tipul JMSProducer) pentru care se va invoca metoda send().

CompletionListener completionListener = new SendMethodListener();
jmsContext.createProducer().setAsync(completionListener).send(destination, message);

Activitate de Laborator

aipi2014-lab09-eclipseee.zip

aipi2014-lab09-netbeans.zip

Pentru rezolvarea acestui laborator sunt necesare:

  • serverul de aplicaţii GlassFish 4.1
  • un mediu integrat de dezvoltare:
    • Eclipse IDE for Java EE Developers 4.4.1 (Luna SR1)
    • NetBeans 8.0.2

Se doreşte dezvoltarea unui sistem de mesagerie (chat) instant utilizând API-ul Java Message Service, format dintr-un server (reprezentat de un obiect administrat de tip javax.jms.Topic) şi mai mulţi clienţi.

Se va lucra în echipe de mai multe persoane. În fiecare echipă pe o maşină va rula serverul, în timp ce pe celelalte maşini vor rula clienţi.

Atât pe server cât şi pe client va rula GlassFish 4.1, aplicația client redirectând apelurile către instanţa de pe server.

Indiferent că rulează pe server (local) sau pe client (la distanţă), aplicaţia JMS va utiliza acelaşi cod sursă, locaţia obiectelor administrate fiind transparentă pentru acesta.

Serverul este reprezentat de o temă, obiect administrat ce are denumirea jms/MessagingTopic.

Clienţii vor fi pentru aceasta atât producători (transmiţând mesaje) cât şi consumatori (primind mesajele care le sunt adresate, fapt asigurat printr-un selector de mesaje, filtrarea făcându-se pe bază de nume de utilizator).

Un mesaj are în antet numele de utilizator al clientului căruia i se adresează, corpul fiind un obiect de tip CustomMessage în care se reţin numele de utilizator al clientului care l-a trimis şi conţinutul.

STRUCTURĂ MESAJ
Antet
Constants.CUSTOM_MESSAGE_PROPERTY = receiverUserName
utilizat în filtrul de mesaje
Conţinut
String senderUserName
String messageContent
CustomMessage

ObjectMessage

1. [0 puncte] Să se descarce arhiva care conține serverul de aplicaţii GlassFish 4.1 și să se despacheteze.

Domeniul domain1 poate fi accesat folosind utilizatorul admin și parola vidă.

2. [0 puncte] Să se integreze mediul de dezvoltare Eclipse IDE for Java EE Developers 4.4.1 (Luna SR1), respectiv mediul de dezvoltare NetBeans 8.0.2 cu serverul de aplicaţii GlassFish 4.1.

Acest pas nu este absolut necesar pentru rularea aplicaţiei, însă configurarea mediilor de dezvoltare este utilă întrucât în acest mod pot fi verificate proprietăţile serverului de aplicaţii precum şi starea acestuia în fiecare moment al execuţiei programului care utilizează specificaţia JMS.

Eclipse IDE for Java EE Developers

Integrarea mediului de dezvoltare Eclipse IDE for Java EE Developers 4.4.1 (Luna SR1) cu serverul de aplicaţii GlassFish 4.1 se face accesând WindowPreferencesServer RuntimeEnvironmentAdd….

În cazul în care GlassFish nu este listat ca optiune de server de aplicaţii care să poată fi configurat, se instalează un adaptor pentru acest tip de server de pe Eclipse Marketplace. După instalarea acestui adaptor, se reporneşte mediul de dezvoltare Eclipse Kepler 4.4.1 for Java EE Developers.

Se creează o referință către JDK din WindowPreferencesJavaInstalled JREs.

Se selectează serverul de aplicaţii GlassFish 4.1 precizându-se directorul în care acesta este instalat.

Se deschide perspectiva asociată serverului de aplicaţii din WindowShow ViewOther…ServerServers.

Se integrează serverul de aplicaţii GlassFish 4.1 în această perspectivă accesând opţiunea Click this link to create a new server… şi indicând locaţia specifică domeniului în care se va lucra, numele de utilizator (admin) şi parola (vidă).

NetBeans

Integrarea mediului de dezvoltare NetBeans 8.0.2 cu serverul de aplicaţii GlassFish 4.1 se face accesând ServicesServerAdd Server….

Se indică tipul de server (GlassFish Server), locaţia la care acesta a fost instalat, precum şi câteva informaţii de configurare (denumirea domeniului local, maşina unde rulează şi informaţiile de autentificare – nume utilizator / parola).

3. [5 puncte] Să se pornească serverul de aplicaţii GlassFish 4.1.

GLASSFISH_HOME/bin> asadmin start-domain domain1
Waiting for domain1 to start .................
Successfully started the domain : domain1
domain  Location: C:\glassfish4\glassfish\domains\domain1
Log File: C:\glassfish4\glassfish\domains\domain1\logs\server.log
Admin Port: 4848
Command start-domain executed successfully.

4. [20 puncte] Să se acceseze consola de administrare a serverului de aplicaţii GlassFish accesibilă la http://localhost:4848.

Server

În DomainAdministrator Password, se va specifica o parolă pentru administrator şi se va apăsa butonul Save.

În ResourcesJMS Resources se vor crea obiectele administrate:

a) Connection FactoriesNew: fabrica de conexiuni MessagingTopicConnectionFactory cu tipul javax.jms.TopicConnectionFactory (se apasă butonul OK).

b) Destination ResourcesNew: resursa destinaţie de tip temă jms/MessagingTopic având tipul javax.jms.Topic (se apasă butonul OK).

Client

Configuraţiile privind obiectele administrate (MessagingTopicConnectionFactory şi jms/MessagingTopic) trebuie realizate întocmai şi pe client, cu precizarea faptului că pentru fiecare dintre acestea se vor specifica nişte proprietăţi suplimentare (Additional Properties), care să redirecteze utilizarea acestor obiecte administrare către corespondentele lor de pe server, motivul definirii lor fiind doar acela al identificării unor referinţe (acestea putând fi accesate local din aplicaţie, nefiind permisă conectarea la distanţă în mod direct).

Se va specifica proprietatea addresslist, având ca valoare adresa IP a maşinii pe care rulează serverul.

Client

În Configurationsserver-configJava Message Service:

a) se specifică tipul de serviciu JMS pentru a putea fi accesat la distanţă (proprietatea JMS Service Type va avea valoarea REMOTE);

Se accesează butonul Save.

b) se indică locaţia la care se află resursele JMS care vor fi accesate (pentru default_JMS_host se va specifica câmpul Host, reprezentând adresa IP a serverului GlassFish); serverul şi clientul trebuie să se găsească în aceeaşi (sub)reţea sau trebuie să aibă adrese IP publice, vizibile în Internet; de asemenea, se va indica parola serverului de aplicaţii GlassFish 4.1 care rulează la locaţia indicată.

Se accesează butonul Save.

5. [5 puncte] Să se adauge bibliotecile lipsă proiectelor Eclipse IDE for Java EE Developers 4.4.1 (Luna SR1) şi NetBeans 8.0.2 astfel încât acestea să poată rula:

Eclipse IDE for Java EE Developers

Messaging ClientBuild PathConfigure Build PathLibrariesAdd External Jars

GLASSFISH_HOME/lib/gf-client.jar (dependenţele către alte referinţe vor fi adăugate automat)

NetBeans

Messaging ClientLibrariesAdd Jar/Folder…

GLASSFISH_HOME/lib/*.jar (4 fişiere)

GLASSFISH_HOME/lib/install/applications/jmsra/*.jar (6 fişiere)

GLASSFISH_HOME/modules/*.jar (274 fişiere)

6. [10 puncte] Să se ruleze aplicaţia, în care numele de utilizator va fi prenume.nume, transmiţându-se mesaje către colegii din cadrul grupei.

7. [50 puncte] La intrarea, respectiv la ieşirea din aplicaţie, să se transmită mesajele Constants.LOGIN_MESSAGE, respectiv Constants.LOGOUT_MESSAGE către toţi utilizatorii cu care s-a comunicat recent pentru a se anunţa faptul că utilizatorul este activ, respectiv inactiv.

Un utilizator care a primit un mesaj că un alt utilizator este disponibil îl va include în lista utilizatorilor activi.

Un utilizator ce a primit un mesaj că un alt utilizator nu mai e disponibil îl va exclude din lista utilizatorilor activi.

a) În clasa ContactsList, în metoda start(), să se transmită mesajul Constants.LOGIN_MESSAGE către toţi utilizatorii cu care s-a comunicat recent (din recentContactsListItem). Se va apela metoda publish() a obiectului communicator având ca parametrii numele de utilizator al destinatarului şi mesajul propriu-zis.

b) În clasa ContactsList, în metoda handleMessage(), să se analizeze mesajul Constants.LOGIN_MESSAGE, parcurgându-se lista connectedContactsListItem şi adăugându-se la aceasta numele de utilizator din mesajul primit, în cazul în care utilizatorul nu se regăseşte în aceasta.

c) În clasa ContactsList, în metoda close(), să se transmită mesajul Constants.LOGOUT_MESSAGE către toţi utilizatorii cu care s-a comunicat recent (din recentContactsListItem). Se va apela metoda publish() a obiectului communicator având ca parametrii numele de utilizator al destinatarului şi mesajul propriu-zis.

d) În clasa ContactsList, în metoda handleMessage(), să se analizeze mesajul Constants.LOGOUT_MESSAGE, parcurgându-se lista connectedContactsListItem şi eliminându-se din ea numele de utilizator din mesajul primit, în cazul în care utilizatorul face parte din aceasta.

8. [10 puncte] Să se modifice comportamentul aplicaţiei astfel încât utilizatorii să poată primi şi mesajele transmise în răstimpul în care nu au fost disponibili.

În clasa PublishSubscribe, în metoda subscribe(), se va modifica tipul obiectului consumator de mesaje astfel încât acesta să fie durabil (persistent şi atunci când este inactiv, adică nu are nici un abonat conectat la el).

9. [10 puncte] Să se modifice formatul mesajelor astfel încât acesta să includă şi ora la care mesajul a fost transmis:

utilizator1(zz1/ll1/aaaa1 hh1:mm1:ss1)> mesaj1
utilizator2(zz2/ll2/aaaa2 hh2:mm2:ss2)> mesaj2

Se va modifica structura mesajului din clasa CustomMessage, modificându-se definiţia metodei handleMessage() din clasa ContactsList astfel încât să conţină şi parametrul referitor la momentul de timp la care a fost transmis mesajul.

Resurse

Soluții

laboratoare/laborator09.txt · Last modified: 2015/01/12 12:57 by Andrei Roșu-Cojocaru
CC Attribution-Share Alike 4.0 International
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0