Filas JMS com JEE 6 e JBoss 7

Nesse post será mostrado como configurar uma fila JMS no JBoss 7.1.0-Final e como utilizá-la através de uma aplicação JEE 6. Além disso, serão apresentados alguns pontos importantes como session transacional e configuração do número de consumidores da fila JMS.

Como exemplo, estará sendo usado o modo standalone do JBoss. Se preferir usar o modo domain, basta alterar os arquivos correspondentes aos apresentados aqui.

JMS

A idéia desse post não é mostrar em detalhes as características do JMS ou da sua especificação, mas vale a pena uma descrição alto nível.
JMS pode ser considerado um MOM (Message Oriented Middleware) que permite o envio e recebimento de mensagens assíncronas entre 1 ou mais clientes e que faz parte da especificação do Java EE. JMS provê dois modelos para troca de mensagens:

  • Queue: É uma fila ponto-a-ponto (point-to-point) na qual uma ponta (produtor) produz uma mensagem e a coloca em uma fila JMS e a outra ponta (consumidor) consome a mensagem da fila. No exemplo desse post, será usado esse modelo.
  • Topic: Segue o modelo publish/subscribe no qual uma ponta (publicador) coloca uma mensagen em um Tópico JMS e, para esse trópico, podem existir 0 ou mais “assinantes” que recebem todas as mensagens publicadas.

Para maiores detalhes, pode-se pesquisar no google ou olhar diretamente o JEE 6 Tutorial

Configurando fila JMS no JBoss

Ao invés de implementar a especificação JMS ele mesmo, o JBoss usa o HornetQ (http://www.jboss.org/hornetq), um projeto open-source que provê um sistema de mensageria bem robusto, performático e escalável. Para configurar uma fila JMS, será necessário alterar o arquivo JBOSS_HOME/standalone/configuration/standalone.xml que, por padrão, não traz o módulo JMS configurado. Sendo assim, será necessário copiar as configurações do arquivo standalone-full.xml que traz essas configurações por default. Abra o arquivo JBOSS_HOME/standalone/configuration/standalone.xml e edite os seguintes pontos:

Adicione o módulo de mensageria dentro de <extensions>

<extensions>
	...
	<extension module="org.jboss.as.messaging"/>
</extensions>

Adicione as configurações do sub-sistema de mensageria após <subsystem xmlns=”urn:jboss:domain:weld:1.0″/>

        <subsystem xmlns="urn:jboss:domain:messaging:1.1">
            <hornetq-server>
                <persistence-enabled>true</persistence-enabled>
                <journal-file-size>102400</journal-file-size>
                <journal-min-files>2</journal-min-files>

                <connectors>
                    <netty-connector name="netty" socket-binding="messaging"/>
                    <netty-connector name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                    </netty-connector>
                    <in-vm-connector name="in-vm" server-id="0"/>
                </connectors>

                <acceptors>
                    <netty-acceptor name="netty" socket-binding="messaging"/>
                    <netty-acceptor name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                        <param key="direct-deliver" value="false"/>
                    </netty-acceptor>
                    <in-vm-acceptor name="in-vm" server-id="0"/>
                </acceptors>

                <security-settings>
                    <security-setting match="#">
                        <permission type="send" roles="guest"/>
                        <permission type="consume" roles="guest"/>
                        <permission type="createNonDurableQueue" roles="guest"/>
                        <permission type="deleteNonDurableQueue" roles="guest"/>
                    </security-setting>
                </security-settings>

                <address-settings>
                    <address-setting match="#">
                        <dead-letter-address>jms.queue.DLQ</dead-letter-address>
                        <expiry-address>jms.queue.ExpiryQueue</expiry-address>
                        <redelivery-delay>0</redelivery-delay>
                        <max-size-bytes>10485760</max-size-bytes>
                        <address-full-policy>BLOCK</address-full-policy>
                        <message-counter-history-day-limit>10</message-counter-history-day-limit>
                    </address-setting>
                </address-settings>

                <jms-connection-factories>
                    <connection-factory name="InVmConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/ConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <connection-factory name="RemoteConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="netty"/>
                        </connectors>
                        <entries>
                            <entry name="RemoteConnectionFactory"/>
                            <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <pooled-connection-factory name="hornetq-ra">
                        <transaction mode="xa"/>
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/JmsXA"/>
                        </entries>
                    </pooled-connection-factory>
                </jms-connection-factories>

                <jms-destinations>
                    <jms-queue name="testQueue">
                        <entry name="queue/test"/>
                        <entry name="java:jboss/exported/jms/queue/test"/>
                    </jms-queue>
                    <jms-topic name="testTopic">
                        <entry name="topic/test"/>
                        <entry name="java:jboss/exported/jms/topic/test"/>
                    </jms-topic>
                </jms-destinations>
            </hornetq-server>
        </subsystem>

Essa é a configuração padrão e para o exemplo será utilizada a própria testQueue que já vem pré-configurada. Caso deseje criar outras filas, basta ir adicionando mais <jms-queue>. Para o exemplo, usaremos o connection-factory e a própria jms-queue.
Sobre esses, precisamos saber que temos 2 ConnectionFactory (um normal e outro transacional) que podem ser acessados pelos nomes java:/ConnectionFactory e java:/JmsXA respectivamente e uma fila que pode ser acessada tanto pelo nome java:/queue/test quanto pelo java:jboss/exported/jms/queue/test.

Uma descrição mais detalhada da configuração pode ser encontrada nas documentações do JBoss e do HornetQ.

Para finalizar a configuração do JBoss, é necessário configurar os socket-binding que foram referenciados em <connectors> e <acceptors>. Vá até a seção <socket-binding-group> e adicione as entradas:

...
        <socket-binding name="messaging" port="5445"/>
        <socket-binding name="messaging-throughput" port="5455"/>
...

Pronto, agora que o JBoss já está devidamente configurado, já podemos ir para a aplicação exemplo.

Aplicação exemplo

Nossa aplicação será bem simples e será empacotada em um simples .war. Ela terá:

  • Um simples POJO (Usuario) com um atributo String nome
  • Um Servlet responsável por criar uma mensagem (Usuario) e enviar para um EJB que fará o envio da mensagem para fila JMS
  • Um MDB responsável por ler mensagens da fila testQueue

Como ferramenta de build será usado o maven. A IDE a ser utilizada como livre escolha.

O pom.xml fica da seguinte maneira:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.wordpress.lucianomolinari</groupId>
	<artifactId>exemplo_jms</artifactId>
	<version>1.0.1</version>
	<packaging>war</packaging>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.3.2</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
				</configuration>
			</plugin>
		</plugins>
	</build>

	<dependencies>
		<dependency>
			<groupId>javax</groupId>
			<artifactId>javaee-api</artifactId>
			<version>6.0</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>

</project>

POJO que será enviado na mensagem JMS

package com.wordpress.lucianomolinari.exemplo_jms.entidade;

import java.io.Serializable;

public final class Usuario implements Serializable {
	private static final long serialVersionUID = 8734596722276424601L;

	private final String nome;

	public Usuario(String nome) {
		this.nome = nome;
	}

	public String getNome() {
		return nome;
	}

}

EJB responsável por fazer o envio da mensagem JMS

package com.wordpress.lucianomolinari.exemplo_jms.jms;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Session;

import com.wordpress.lucianomolinari.exemplo_jms.entidade.Usuario;

@Stateless
public class ProdutorDeMensagem {
	@Resource(mappedName = "java:/ConnectionFactory")
	private ConnectionFactory connectionFactory;

	@Resource(mappedName = "java:/queue/test")
	private Destination destination;

	private Connection connection;
	private Session session;
	private MessageProducer messageProducer;

	@PostConstruct
	public void init() {
		try {
			connection = connectionFactory.createConnection();
			session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
			messageProducer = session.createProducer(destination);
		} catch (JMSException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	@PreDestroy
	public void destroy() {
		if (connection != null) {
			try {
				connection.close();
			} catch (JMSException e) {
				e.printStackTrace();
			}
		}
	}

	public void enviarMensagem(Usuario usuario) {
		ObjectMessage message;
		try {
			message = session.createObjectMessage(usuario);
			messageProducer.send(message);
		} catch (JMSException e) {
			e.printStackTrace();
		}
	}

}

MDB responsável por receber as mensagens JMS

package com.wordpress.lucianomolinari.exemplo_jms.jms;

import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.ObjectMessage;

import com.wordpress.lucianomolinari.exemplo_jms.entidade.Usuario;

@MessageDriven(activationConfig = {
		@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
		@ActivationConfigProperty(propertyName = "destination", propertyValue = "java:/queue/test") })
public class ConsumidorDeMensagem implements MessageListener {

	@Override
	public void onMessage(Message message) {
		ObjectMessage objMsg = (ObjectMessage) message;
		try {
			System.out.println("<<<<<<<<Recebendo mensagem com o usuario com nome "
					+ ((Usuario) objMsg.getObject()).getNome());
		} catch (JMSException e) {
			e.printStackTrace();
		}
	}

}

Sevlet que chama o produtor da mensgaem

package com.wordpress.lucianomolinari.exemplo_jms.web.servlet;

import java.io.IOException;

import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.wordpress.lucianomolinari.exemplo_jms.entidade.Usuario;
import com.wordpress.lucianomolinari.exemplo_jms.jms.ProdutorDeMensagem;

@WebServlet("/test")
public class ServletCriaMensagem extends HttpServlet {
	private static final long serialVersionUID = -973490316445738120L;

	@Inject
	private ProdutorDeMensagem produtorDeMensagem;

	@Override
	protected void doGet(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
		System.out.println(">>>>Criando mensagem de teste..");
		produtorDeMensagem.enviarMensagem(new Usuario("Usuario Teste"));
	}

}

Acessando a URL http://localhost:8080/exemplo_jms/test é possível perceber a mensagem sendo enviada e consumida.

Detalhes Importantes

Session Transacional

Quando é criado uma session JMS, o primeiro parâmetro informado (boolean) é se aquela session deve ou não ser transacional. No exemplo acima, criamos a session sem ser transacional:

session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

Porém nem sempre esse é o cenário desejado, principalmente porque em muitas vezes o envio da mensagem JMS estará dentro de uma transação que faz outras tarefas, como atualizar o BD por exemplo. Então para transformar a session em transacional, bastaria passar true como parâmetro:

session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);

Infelizmente se você fizer apenas isso e rodar a aplicação, vai perceber que a mensagem nunca é enviada/consumida da fila. Isso ocorre pois o ConnectionFactory que foi usado (java:/ConnectionFactory) não foi configurado no JBoss para ser transacional. Para fazer com que a mensagem seja enviada normalmente com o session transacional, existem 2 abordagens:

Dar o commit explicitamente

Uma forma é explicitamente dar o commit na session após o envio da mensagem:

	public void enviarMensagem(Usuario usuario) {
		ObjectMessage message;
		try {
			message = session.createObjectMessage(usuario);
			messageProducer.send(message);
			session.commit();
		} catch (JMSException e) {
			e.printStackTrace();
		}
	}

Porém essa abordagem não é recomendada, pelo mesmo motivo que não é recomendado usar uma session não transacional: o envio da mensagem pode fazer parte de uma transação maior. Mas qual seria a solução então?

Usar um ConnectionFactory que suporte transação

Existe um outro ConnectionFactory configurado no JBoss (java:/JmsXA) que está configurado como transacional (<transaction mode=”xa”/>). Então pegando nosso código inicial, as únicas mudanças necessárias seriam:

Alterar o mappedName do nosso ConnectionFactory para java:/JmsXA

	@Resource(mappedName = "java:/JmsXA")
	private ConnectionFactory connectionFactory;

E criar a session como transacional

session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);

Com isso o envio de mensagens volta a funcionar corretamente e o melhor: ele pode fazer parte de uma transação que realiza outras tarefas.

Configurando do número de consumidores da fila JMS

Para fechar esse post, será mostrado agora uma configuração simples, mas que pode ajudar muito na performance/escalabilidade da sua aplicação no que diz respeito ao consumo de mensagens de uma fila JMS.

Imagine o seguinte cenário:

  • O processamento de uma mensagem pelo seu MDB é um tanto quanto pesado e pode levar alguns segundos (5s por exemplo)
  • A quantidade de mensagens que são colocadas na fila é “grande” (10 mensagens por segundo)
  • Você tem apenas 5 instâncias de MDB consumindo as mensagens

Considerando um período de 10 segundos, sua fila teria recebido 100 mensagens (10 por segundo) e sua aplicação teria consumido apenas 10 mensagens, já que cada MDB teria consumido 2 mensagens (10s / 5s) * 5 instâncias de MDBs. Veja que isso pode se tornar um gargalo.
Para configurar a quantidade de instâncias do MDB, basta abrir o arquivo de configuração do JBoss (standalone.xml) e ir até a sessão do subsistema <subsystem xmlns=”urn:jboss:domain:ejb3:1.2″>.

..
            <mdb>
                <resource-adapter-ref resource-adapter-name="hornetq-ra"/>
                <bean-instance-pool-ref pool-name="mdb-strict-max-pool"/>
            </mdb>
            <pools>
                <bean-instance-pools>
                    <strict-max-pool name="slsb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
                    <strict-max-pool name="mdb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
                </bean-instance-pools>
            </pools>
..

Veja que você tem a linha <bean-instance-pool-ref pool-name=”mdb-strict-max-pool”/> que indica que para MDB será usado o pool mdb-strict-max-pool e logo abaixo tem sua configuração:

<strict-max-pool name="mdb-strict-max-pool" max-pool-size="20" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>

O default do JBoss é de até 20 instâncias para cada MDB. Quando achar conveniente aumentar/diminuir esse valor, basta alterar o valor do atributo max-pool-size.

Conclusão

O uso de JMS com JEE 6 é simples e exige apenas algumas anotações e poucas linhas de código. Apesar disso, é um mecanismo bem robusto e tem a vantagem de poder fazer com que o envio de uma mensagem faça parte de uma transação, além de fazer parte da Espec do JEE, o que significa que sua aplicação poderia rodar em outro servidor sem maiores problemas.

A configuração do JBoss é um pouco chata de início, pois o arquivo standalone.xml não vem com o módulo de mensageria habilitado por padrão. Mas após a habilitação do módulo, as demais configurações se tornam simples, como criar filas/tópicos e configurar o pool de MDBs.

O código apresentado pode ser encontrado aqui e qualquer dúvida é só usar o espaço de comentários.


Quer aprender muito mais? Não deixe de ver meu curso Construa uma aplicação do zero com JEE 7, Java 8 e Wildfly.

Anúncios
Esta entrada foi publicada em Java EE, JBoss com as etiquetas , , , , , , . ligação permanente.

29 respostas a Filas JMS com JEE 6 e JBoss 7

  1. Odair diz:

    Boa noite Luciano. Eu refiz o exemplo aqui em casa, também utilizando o JBOSS AS 7, mas para que a injeção funcionasse na servlet, precise adicionar a anotação @LocalBean ao Bean ProdutorMensagem:

    @Stateless
    @LocalBean
    public class ProdutorMensagem{
    //resto da implementação
    }

    No mais, funcionou que foi uma maravilha. Eu particularmente não gostei da abordagem do AS 7 em relação a libs, onde agora é necessário configurar um módulo e declarar quais bibliotecas desejamos utilizar. É verdade que isso torna as coisas mais organizadas e o “Jar Hell” acaba, mas ficou muito trabalhoso fazer isso.

    Eu gostei bastante do novo modo administrativo via browser. Pretendo utilizá-lo a partir de agora.

    Ótimo artigo, parabéns.

    • Bom dia Odair,
      Quando testei em casa, não precisei anotar com @LocalBean, de qualquer forma, o importante é que funcionou. Depois vou até testar novamente para validar.
      Realmente, essa mudança na forma de utilização de lib trouxe vantagens e desvantagens, principalmente para quem estava acostumado com o modelo antigo, que é meu caso. Mas no geral, o JBoss 7 ficou bem legal, mais rápido, esse console administrativo via browser tá bem melhor, agora tem console via linha de comando..enfim, bastante coisa nova.
      Obrigado pelo feedback, abraços.

  2. Pingback: Processamento assíncrono com ExecutorServices | Software, Java e Arquitetura

  3. Gaspar diz:

    Parabéns pelo artigo.

  4. Jr. diz:

    Luciano quando eu tento startar o projeto ele da.
    Exception in thread “main” java.lang.NullPointerException

    bem na hora de abrir a conexão.
    Connection connection = connectionFactory.createConnection();

    Alguma ideia?
    Excelente artigo Obrigado!

  5. Robert Lazaro Resende diz:

    Boa tarde,

    luciano parabéns pelo post… me quebrou um galho aqui rapidamente solucionei um problema.

    Abraço

  6. Curti teus posts! Estão me ajudando pra caraio!

  7. Xilon diz:

    Amigo, primeiro parabéns pelo post.

    Eu estou tendo nullpointer nessa linha: produtorDeMensagem.enviarMensagem(new Usuario(“Usuario Teste”)); na hora de tentar rodar o servlet. Eu vi que faz sentido dar nullpointer uma vez que no seu código vc não inicializa a variável. Gostaria de saber o que estaria ocorrendo.

    Abraço

    • Xilon,
      Como a variável produtorDeMensagem é anotada com @Inject, ela é inicializada automaticamente pelo container, nesse caso JBoss 7. Você está rodando a aplicação no JBoss?

      Abraço

      • Xilon diz:

        Opa Luciano, obrigado pela resposta. Bom eu acreditava que sim. Eu criei o projeto como Maven e mandei mandei rodar “Run on server” num servidor JBoss 7.1 Runtime Server, achei que isso seria o suficiente. O que eu estaria fazendo de errado?

        Abraço

      • Oi Xilon,
        No exemplo subi o JBoss sem o Eclipse. Tente usar o código disponível para download e subir o JBoss sem usar o eclipse (após fazer as configurações descritas no artigo).
        Com isso deve funcionar.

        Abraço

  8. Xilon diz:

    Eu queria adicionar uma coisa: para conseguir fazer subir o server JBoss eu precisei adicionar a tag mdb no standalone:

    Isso é alguma configuração nova?

  9. Jonathas J. C. diz:

    Olá, muito obrigado pelo artigo.
    Essa configuração funciona para chamadas remota? Ex. Caso um servidor em outra instância queira consumir JMS.

  10. Bruno diz:

    Eu estou tendo nullpointer nessa linha: produtorDeMensagem.enviarMensagem(new Usuario(“Usuario Teste”)); na hora de tentar rodar o servlet. Eu vi que faz sentido dar nullpointer uma vez que no seu código vc não inicializa a variável. Gostaria de saber o que estaria ocorrendo.

    Nas respostas tem escrito subir o JBoss sem usar o eclipse . Como se faz isso???

    Ja tentei pelo arquivo run.bat e não funciona.

    Att

    • Olá Bruno,
      A variável é inicializada automaticamente pelo conteiner, conforme especificação do Java EE.

      Sobre a inicialização do JBoss sem o Eclipse, é só você chamar o script standalone.sh (ou .bat), que está na mesma pasta do run.bat.

      Att.

    • Daniel diz:

      Isso é por causa do CDI, provavelmente voce nao criou o arquivo beans.xml dentro da pasta WEB-INF, é só criar um arquivo beans.xml vazio e colocar nesta pasta.

  11. Olá Luciano, tenho um cenário em que rodo várias instâncias do jBoss, e cada uma destas instâncias roda mais de uma instância da minha aplicação, é possível utilizar JMS neste cenário ?

  12. Olá Luciano, tenho um método anotado com @Transaction que realiza operações na base de dados e em seguida envia mensagens JMS para uma fila. Gostaria que o método responsável pelo envio de mensagens usasse a mesma transação já aberta. Podes dar uma força?

    • Ola Iury,
      Nesse caso você precisa usar uma session transacional e com o connection factory XA. Além disso, seu data source com o banco de dados também precisa ser XA.
      Abraços,
      Luciano

  13. Excelente post! Parabens! Salvou vidas! 😀

Deixe uma Resposta

Preencha os seus detalhes abaixo ou clique num ícone para iniciar sessão:

Logótipo da WordPress.com

Está a comentar usando a sua conta WordPress.com Terminar Sessão / Alterar )

Imagem do Twitter

Está a comentar usando a sua conta Twitter Terminar Sessão / Alterar )

Facebook photo

Está a comentar usando a sua conta Facebook Terminar Sessão / Alterar )

Google+ photo

Está a comentar usando a sua conta Google+ Terminar Sessão / Alterar )

Connecting to %s