Exemplo Spring JMS: envio de e-mail assíncrono

Ao desenvolver nossos sistemas, nos deparamos as vezes com um problema: alguma funcionalidade é muito pesada em termos de processamento e não precisa necessariamente ser feita de forma sincronizada com o resto da lógica de negócio, por exemplo, envio de e-mails, envio de SMS, etc.

Nestes casos, para diminuir o tempo que o usuário espera pela resposta do sistema, é conveniente tornar assíncrono o processamento destas tarefas em relação ao resto da lógica. Uma estratégia comumente usada é utilizar filas de mensagens. Nestes casos a lógica principal de negócio em vez de chamar a rotina que envia o e-mail, só chama uma rotina que insere uma mensagem na fila específica de envio de e-mails. Enquanto isso, temos uma rotina “consumidora” desta fila, desatrealada da lógica principal, que “consumirá” as mensagens, tratando-as de forma adequada conforme sua disponibilidade, de forma assíncrona ao resto da aplicação.

Spring provê um framework que abstrai e simplifica o uso da API JMS (Java Message Service). Particularmente para este post, utilizaremos este mecanismo do Spring em conjunto com o suporte do Spring para POJOs dirigidos a mensagens (um modo de receber mensagens que se parece com beans orientado a mensagens Message-Driven BeansMDBs da especificação EJB). mas, que ao contrário do MDB nos permite utilizar um servidor web comum, como Tomcat, por exemplo.

Introdução ao JMS

O JMS é uma especificação do Java (JSR 914: Java Message Service) responsável por fornecer uma opção de comunicação não-síncrona, de forma que o cliente não precise esperar pelo processamento do serviço. O cliente envia a mensagem e segue com a premissa de que o serviço receberá e processará a mensagem.

No JMS, quando um aplicativo envia informações (mensagens) para outro, eles não tem uma ligação direta, ao invés disso ele a entrega a um intermediário de mensagens que a entregará à aplicação de destino especificada.

Há dois tipos de destinos:

Filas (Modelo Mensagem Ponto a Ponto): Uma mensagem é enviada a uma fila, onde será posteriormente consumida e retirada da fila. Apesar de ser processada por apenas um consumidor, podem existir vários consumidores aguardando para uma mesma fila. Podemos fazer um paralelo com fila de banco: cada cliente é atendido apenas uma vez, mas podem haver vários caixas atendendo, e o cliente não tem como saber a priori qual caixa lhe atenderá.

Tópicos (Modelo Publicação-Inscrição): Assim com em filas, uma mensagem é enviada ao tópico, e podem existir vários consumidores. Mas a diferença é que uma cópia da mensagem será entregue a cada consumidor daquele tópico. Paralelo: Assinatura de revistas, uma revista é publicada e cada assinante daquela revista recebe um exemplar.

Intermediário de mensagens: Apache ActiveMQ

Como dito no item anterior, no JMS uma mensagem deve ser entregue a um intermediário de mensagens (middleware), que será o responsável por entregá-la ao destino (fila ou tópico respectivo).

Uma solução interessante de intermediário de mensagens de código aberto é o Apache ActiveMQ, que utilizaremos neste exemplo.

Utilizando o Spring

Apenas com o ActiveMQ, sem o Spring, teríamos que programaticamente criar a conexão, criar uma sessão, criar a fila/tópico de destino, a mensagem, enviar, etc. O que daria um bocado de código repetitivo para cada vez que criássemos uma fila/tópico. Mas com o Spring podemos utilizar o JmsTemplate, que usa o ApacheMQ e ajuda a abstrair muito código repetitivo.

Exemplo

Baixe o projeto Eclipse ou o projeto Maven deste exemplo.

A idéia deste exemplo é simular uma rotina de envio de e-mail, fazendo com que a aplicação envie e-mails de forma assíncrona ao resto da lógica da aplicação. Esta abordagem é interessante neste caso, porque em geral envio de e-mails são custosos, e tornam a resposta ao usuário lenta e desconfortável.

Primeiro, vamos incluir o spring no projeto, incluindo suas libs e incluindo a configuração necessária no web.xml:

<!–- Arquivo configuracao spring –->
<context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>WEB-INF/spring-jms.xml</param-value>
</context-param>

Depois do Spring configurado, para fazer tudo funcionar, primeiramente precisaremos criar algumas configurações relativas ao JMS no arquivo xml de configurações do Spring (que chamaremos de spring-jms.xml).

Neste arquivo de configuração, precisamos criar uma fábrica de conexões JMS para estar apto a enviar mensagens através do intermediário:

<bean id="connectionFactory">
     <property name="brokerURL" value="vm://localhost" />
</bean>

Além da fábrica, precisamos declarar um destino (uma fila ou tópico) para as mensagens:

Fila:

<bean id="filaEmail">
     <constructor-arg index="0" value="br.com.nessauepa.jms.filas.email">
</bean>

Em seguinda, precisamos declarar o jsmTemplate como um bean, passando como parâmetro a conexão do ActiveMQ criada anteriormente:

<bean id="jsmTemplate" class= "org.springframework.jms.core.JmsTemplate">
     <property name="connectionFactory" ref="connectionFactory" />
</bean>

Enviando uma mensagem

Abaixo a classe produtora que trata o envio da mensagem para a fila de mensagens relativas a e-mails:

package br.com.nessauepa.jms;

import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

public class ProdutorFilaEmail {

    private JmsTemplate jmsTemplate;

    private Destination destination;

    public void setJmsTemplate(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void setDestination(Destination destination) {
        this.destination = destination;
    }

    public void enviarMensagem(final String to, 
                               final String subject,
                               final String content, 
                               final String project) {
        // messageCreator
        MessageCreator messageCreator = new MessageCreator() {

            public Message createMessage(Session session) throws JMSException {

                MapMessage message = session.createMapMessage();
                message.setString("to", to);
                message.setString("subject", subject);
                message.setString("content", content);
                message.setString("project", project);

                return message;
            }
        }

        // envia mensagem
        jmsTemplate.send(destination, messageCreator);
    }
 }

Agora criamos a configuração dessa classe produtora no nosso spring-jms.xml, injetando os recursos necessários para a classe, que são a instância do jmsTemplate e o destino, que é a fila de emails configurada anteriormente:

<!-- Produtor -->
<bean id="produtorFilaEmail">
     <property name="jmsTemplate" ref="jsmTemplate" />
<property name="destination" ref="filaEmail" />
</bean>

Para testar a criação da mensagem e envio para a fila de forma simples, usaremos um servlet que será invocado por um link “Enviar e-mail” de um jsp comum:

package br.com.nessauepa.web;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import br.com.nessauepa.negocio.EmailBO;

public class EmailServlet extends HttpServlet {

	protected void doGet(HttpServletRequest request, HttpServletResponse response) 
                            throws ServletException, IOException {

		// injeta BO
		WebApplicationContext springContext =
                           WebApplicationContextUtils
                           .getWebApplicationContext(getServletContext());

		EmailBO emailBO =(EmailBO)springContext.getBean("emailBO");

		emailBO.enviarEmail();

		response.sendRedirect("sucesso.jsp");
	}
}

Recebendo uma mensagem

De forma análoga ao método send da classe JmsTemplate, poderíamos usar o método receive para receber uma mensagem na classe consumidora (classe que trataria o recebimento da mensagem), mas o método receive é sincronizado, o que implicaria bloquear a rotina chamadora, que não é o que queremos. Para o nosso caso, de recebimento assíncrono, utilizaremos um POJO dirigido a mensagens do Spring, que é análogo aos Message Driven Beans do EJB.

Na prática este POJO dirigido a mensagens é uma classe que implementa a interface “MessageListener” do JMS e o método onMessage(Message message), que trata o recebimento da mensagem.

Abaixo a classe que trata o recebimento assíncrono da mensagem para a fila de mensagens relativas a e-mails:

package br.com.nessauepa.jms;

import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageListener;

public class ListenerFilaEmail implements MessageListener {

    public void onMessage(Message message) {
        MapMessage mapMessage = (MapMessage) message;

        try {
            // obtem dados da mensagem
            String to = mapMessage.getString("to");
            String subject = mapMessage.getString("subject");
            String content = mapMessage.getString("content");
            String project = mapMessage.getString("project");

            // envia email

           /* TODO: aqui voce chama sua rotina de envio de email passando
           *  os dados recebidos pela mensagem
           */
           try {
               Thread.sleep(5000); // simula tempo de envio de e-mail
           } catch (InterruptedException e) {
               e.printStackTrace();
           } 

           System.out.println("E-mail enviado.");

       } catch (JMSException e) {
           e.printStackTrace();
       }
   }
 }

Agora criamos a configuração dessa classe listener no nosso spring-jms.xml:

<bean id="listenerFilaEmail" class= "br.com.nessauepa.jms.ListenerFilaEmail"/>

E injetamos os recursos necessários no container deste listener, que são a fábrica de conexões do ActiveMQ, o destino, que é a fila de e-mails pré-configurada e o listener especificado acima.

Um container de listener de mensagem é um bean especial que assiste ao destino JMS, esperando uma mensagem chegar. Uma vez que a mensagem chegue, recupera-a e passa-a numa implementação MessageListener, chamando o método onMessage).

Para este exemplo utilizamos o tipo de container “SimpleMessageListenerContainer”, que é o tipo mais simples, trabalha com um número fixo de seções de JMS e não tem suporte a transações. No Spring existem outros tipos de container mais poderosos, mas para este exemplo utilizaremos o mais simples.

<!-- Container de Listeners -->
<bean class= "org.springframework.jms.listener.SimpleMessageListenerContainer">
     <property name="delegate" ref="connectionFactory" />
     <property name="destination" ref="filaEmail" />
     <property name="messageListener" ref="listenerFilaEmail" />
</bean>

Evoluindo o exemplo

Como alternativa ao POJO que implementa a interface “MessageListener” e o método onMessage, poderíamos evoluir este exemplo utilizando um POJO comum com um método de negócio comum que tratasse o recebimento da mensagem como meuMetodo(MapMessage message) e alterando a configuração acima para delegar o tratamento da mensagem para o método que desejarmos:

<bean class= "org.springframework.jms.listener.adapter.MessageListenerAdapter">
     <property name="delegate" ref="listenerFilaEmail" />
     <property name="defaultListenerMethod" ref="meuMetodo" />
</bean>

Outra alteração que poderia melhorar a qualidade da implementação seria converter a mensagem do tipo genérico javax.jms.MapMessage para uma própria do negócio, adicionado conversão de mensagens ao container como se segue:

<bean class= "org.springframework.jms.listener.adapter.MessageListenerAdapter">
     <property name="delegate" ref="listenerFilaEmail" />
     <property name="defaultListenerMethod" ref="meuMetodo" />
     <property name="messageConverter" ref="emailMessageConverter" />
</bean>

Este emailMessageConverter seria um bean responsável por converter a mensagem para um tipo criado.

Após estas modificações a classe “listener” seria verdadeiramente um POJO, sem qualquer dependência em qualquer tipo JMS. A classe seria basicamente simples assim:

public class ListenerFilaEmail {

    public void meuMetodo(MeuEmail email) {
    }
}

Conclusão

Agora você já viu como Spring tem suporte para sistema de mensagens através da abstração JMS. Enviamos e recebemos mensagens sem ter de sucumbir às complexidades da API de JMS ou recorrendo ao uso de Message Driven Beans do EJB.

Com isso completamos nosso exemplo de uso de fila de mensagens com o Spring JMS. Até a próxima.

Referências

Livro “Spring Em Ação” – Craig Walls
Site “Spring JMS (Java Message Service)”
Site “Simple Spring JMS Example
Site “Apache ActiveMQ
Site “JavaBeat – Integrating Spring with JMS

 

One comment on “Exemplo Spring JMS: envio de e-mail assíncrono

  1. @nessauepa (ou autor do post),

    Parabéns pelo post, muito bom mesmo!!! xD
    P/ ser sincero, não sabia que era possível enviar E-Mail fazendo uma chama assíncrona usando uma implementação tão simples e prática assim.

    Porem vc mencionou “mas, que ao contrário do MDB nos permite utilizar um servidor web comum, como Tomcat, por exemplo”: e como seria isto? Impl usando HTTP Socket’s?!!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>