Java Message Service, peer-to-peer, publish-subscribe, loosely coupled systems, message, topic, queue, connection factories, destinations, JNDI, resource injection
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.
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:
Î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.
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.
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:
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();
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);
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.
O aplicaţie JMS este formată din următoarele componente:
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).
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:
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.
Caracteristicile unui sistem publicare-abonare sunt:
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:
receive()
;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.
onMessage()
, ce acţionează asupra conţinutului.onMessage()
), cu diferenţa că aceasta nu trebuie înregistrată la un anumit consumator.
Principalele componente ale unei aplicaţii JMS sunt:
JMSContext
); 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.
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ă.
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.
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.
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.
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.
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);
createBrowser()
suportă şi un alt argument, indicând un selector de mesaje.
Î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.
PERSISTENT
în mod implicit; astfel de mesaje nu vor fi pierdute în cazul producerii unei erori la nivelul furnizorului JMS.
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.
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:
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.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ă);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).
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()
.
În momentul în care este transmis un mesaj folosind sistemul Java Message Service, pot fi configurați diverși parametri, care indică:
send()
.
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);
PERSISTENT
.
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.
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);
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);
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.
0
, mesajul nu va expira niciodată.
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.
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.
Mecanismele de tip cerere/răspuns reprezintă situații pentru care este adecvată implementarea unor destinații temporare:
JMSReplyTo
din cadrul antetului mesajului ca fiind locația la care trebuie primit răspunsul;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);
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); } // ... }
O tranzacție grupează o serie de operații într-o unitate de lucru atomică.
Î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.
Metoda rollback()
a unui obiect de tip JMSContext
va fi folosită pentru a se reveni la starea anterioară unei tranzacții.
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 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
.
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.
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()
.
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.
onException()
, care va fi invocată în mod automat dacă operația send()
eșuează.
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);
Pentru rezolvarea acestui laborator sunt necesare:
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 | |
---|---|
AntetConstants.CUSTOM_MESSAGE_PROPERTY = receiverUserName | utilizat în filtrul de mesaje |
ConţinutString 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.
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 Window → Preferences → Server Runtime → Environment → Add….
Î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 Window → Preferences → Java → Installed 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 Window → Show View → Other… → Server → Servers.
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ă).
Integrarea mediului de dezvoltare NetBeans 8.0.2 cu serverul de aplicaţii GlassFish 4.1 se face accesând Services → Server → Add 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.
În Domain → Administrator Password, se va specifica o parolă pentru administrator şi se va apăsa butonul Save.
În Resources → JMS Resources se vor crea obiectele administrate:
a) Connection Factories → New: fabrica de conexiuni MessagingTopicConnectionFactory
cu tipul javax.jms.TopicConnectionFactory
(se apasă butonul OK).
b) Destination Resources → New: resursa destinaţie de tip temă jms/MessagingTopic
având tipul javax.jms.Topic
(se apasă butonul OK).
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.
În Configurations → server-config → Java 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:
Messaging Client → Build Path → Configure Build Path → Libraries → Add External Jars
GLASSFISH_HOME/lib/gf-client.jar
(dependenţele către alte referinţe vor fi adăugate automat)
Messaging Client → Libraries → Add 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.
Eric Jendrock, Ricardo Cervera-Navarro, Ian Evans, Kim Haase, William Markito - The Java EE 7 Tutorial - capitolul 45