Jakarta Messaging - antes conhecida apenas como Java Message Service - (JMS) é a API que especifica como aplicações Java Enterprise enviam e recebem mensagens através de um Message Oriented Middleware (MOM). MOMs são um componente essencial para integração de operações entre sistemas corporativos diferentes.
Mensagens
O conceito de mensagem possui uma definição bastante ampla na computação. No contexto do JMS, mensagens são requisições assincronas ou eventos que são produzidos ou consumidos pelas aplicações. Essas mensagens geralmente contêm informações vitais necessárias para a coordenação entres os diferentes sistemas.
Arquitetura do JMS
Em um alto nível, a arquitetura do JMS consiste nos seguintes componentes:
- Provider: JMS é apenas uma API, então ela precisa de uma implementação que efetivamente direcione as mensagens, ou seja, o provider, também conhecido como message broker
- Client: a aplicação que produz ou consome mensagens atraves de algum provider
- Messages: os objetos que os clientes enviam ou recebem dos providers
- Administered Objects: objetos disponibilizados pelos brokers ao cliente (conexão e destino)
Os providers habilitam a comunicação assíncrona disponibilizando um destino, que é o lugar onde as mensagens ficam até que sejam consumidas pelo cliente. Com base no destino, há 2 tipos de modelos de envio de mensagens:
- Point to Point (P2P): 1 mensagem destinada a um único consumidor
- Publish-subscribe (pub-sub): 1 mensagem para N consumidores
modelo P2P |
modelo pub-sub |
Hello JMS
Criemos agora um exemplo de uma aplicação JEE que:
- através de um timer, envia uma mensagem para uma fila chamada HelloQueue a cada 3 segundos
- um tópico chamado PurchaseTopic que é observado pelo Operador de Cartão de Crédito e pelo Departamento Financeiro, ou seja, toda vez que uma operação de cartão de crédito chega nesse Tópico, as respectivas áreas são notificadas.
Como servidor de aplicação, usaremos Payara Application Server, que tem como message broker default o OpenMQ instalado na pasta <PAYARA_HOME>/mq/bin
Para iniciar o payara executamos o binário asadmin localizado em
<PAYARA_HOME>/glassfish/bin
depois o comando start-domain
O console administrativo do payara pode ser acessado por padrão na porta 4848
No painel esquerdo, clique em JMS resources, depois em destination resources
Clique em new. Vamos criar um destino do tipo fila chamado HelloQueue. Preencha os campos conforme a imagem abaixo e clique em save
De forma analoga, criamos um tópico chamado PurchaseTopic
Agora temos agora 2 Destination Resources. Seus respectivos JNDI names serão usados por qualquer aplicação que queira se conectar aos mesmos e receber as mensagens que chegarem
Enviando Mensagens para jms/HelloQueue
Vamos chamar de TimedProducer o producer que envia mensagens para jms/HelloQueue a cada 3 segundos.
@Stateless
@LocalBean
public class TimedQueueProducer {
@Inject
private JMSContext jmsContext;
@Resource(lookup = "jms/HelloQueue")
private Queue queue;
@Schedule(hour = "*", minute = "*", second = "*/3", info = "Every 3 seconds", timezone = "UTC", persistent = false)
public void sendToQueue() {
TextMessage textMessage = jmsContext.createTextMessage("New Text Message");
JMSProducer producer = jmsContext.createProducer();
producer.send(queue, textMessage);
}
}
TimedProducer é um EJB, o método sendToQueue é anotado com @Schedule cujos parâmetros configuram para que seja chamado a cada 3 segundos. Esse método cria uma mensagem do tipo texto e envia para a queue.
Depois criamos uma classe que consome cada mensagem enviada para HelloQueue. Chamamos de QueueReceiver.
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/HelloQueue")
})
public class QueueReceiver implements MessageListener {
private final static Logger LOG = Logger.getLogger(QueueReceiver.class.getName());
@Override
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
LOG.info(">>> received: " + textMessage.getText());
}
catch (JMSException e) {
e.printStackTrace();
}
}
}
QueueReceiver é anotada com @MessageDriven, o que a torna um consumidor assíncrono, isto é, o método onMessage é chamado pelo container sempre quando uma nova mensagem chega na fila especificada.
Quando a mensagem chega, ela é registrada nos logs do payara, localizado em <PAYARA_HOME>/glassfish/domain/domain1logs/server.log
Enviando Mensagens para jms/PurchaseTopic
Nesse próximo exemplo, criamos uma servlet simulando o envio de dados de um cartão de crédito para o tópico chamado jms/PruchaseTopic o qual é observado pelo microserviço do Departamento financeiro e pela operadora de cartão (nesse exemplo são 2 classes dentro do projeto, mas elas poderiam estão em outra aplicação, em outro servidor).
@WebServlet(urlPatterns = "/hello")
public class ViewServelt extends HttpServlet {
@EJB
private TopicProducerService topicProducer;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cvv = req.getParameter("cvv");
String number = req.getParameter("number");
topicProducer.sendMessage(new CreditCard(cvv, number));
resp.getWriter().println("<h1<Credit Card sent to the card operator topic...</h<");
}
}
A servlet recebe no path os parametros CVV e number e em seguida os envia para o TopicProducer, o qual por sua vez publica os dados do cartão no tópico o que faz com que o provider notifique todas as aplicações envolvidas, como no exemplo:
@Stateless
@LocalBean
public class TopicProducerService {
@Inject
private JMSContext context;
@Resource(lookup = "jms/PurchaseTopic")
private Topic helloTopic;
public void sendMessage(CreditCard creditCard) {
JMSProducer producer = context.createProducer();
producer.send(helloTopic, creditCard);
}
}
As classes CardOperator e FinancialDepartament observam o tópico PurchaseTopic e logam os dados do cartão toda vez que eles chegam no tópico:
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/PurchaseTopic")
})
public class CardOperator implements MessageListener {
private static final Logger LOG = Logger.getLogger(CardOperator.class.getName());
@Override
public void onMessage(Message message) {
try {
CreditCard cc = message.getBody(CreditCard.class);
LOG.info(String.format("Received cc: %s", cc ));
}
catch (JMSException e) {
e.printStackTrace();
}
}
}
E a classe FinancialDepartment:
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/PurchaseTopic")
})
public class FinancialDepartament implements MessageListener {
private static final Logger LOG = Logger.getLogger(FinancialDepartament.class.getName());
@Override
public void onMessage(Message message) {
try {
CreditCard cc = message.getBody(CreditCard.class);
LOG.info(String.format("Received cc: %s", cc ));
}
catch (JMSException e) {
e.printStackTrace();
}
}
}
Ao chamarmos a url da servlet, os dados do cartão são registrados nos logs pelos consumidores do tópico
http://localhost:8080/hello-jms/hello?cvv=789&number=333333333333
o consumo dos tópicos é registrado nos logs
O código completo se encontra no github.