Empezar con JMS y Spring

De ChuWiki

Vamos a ver un ejemplo de cómo usar JMS con Spring Framework. Tienes el código completo del ejemplo en spring-jms-example

Será un servidor (JmsServerMain) que publique datos en JMS y un cliente (JmsGuiMain) que reciba dichos datos.

Común a Servidor y Cliente[editar]

JMS Connection Factory[editar]

Dentro de nuestro fichero spring xml donde definimos los beans, definimos una connection factory para la conexión con nuestro servidor de JMS (en nuestro ejemplo, usaremos ActiveMQ). El XML sería este

    <bean id="jmsConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL" value="tcp://localhost:61616"/>
    </bean>

La clase ActiveMQConnectionFactory está en el jar org.apache.activemq:activemq-core, así que tendremos que poner esa dependencia. El atributo brokerURL dice dónde está antendido el servidor JMS, en localhost, puerto 61616 en nuestro caso.

JmsTemplate[editar]

Spring nos ofrece la clase JmsTemplate para facilitarnos el trabajo con JMS, que suele ser algo tedioso. Esta clase tiene métodos para enviar datos por JMS o para recibirlos. En nuestro ejemplo solo la vamos a usar para enviar. Para recibir usaremos otro mecanismo más directo que explicamos más adelante.

Una instancia de JmsTemplate sólo puede tratar bien con "topic" de JMS o bien con "queue".

Un "topic" de JMS es una simple cadena de texto, la que nosotros queramos, donde publicamos datos. Los que quieran recibir esos datos, deben suscribirse a ese topic y recibirán los datos que ahí se publiquen. Puede haber muchos suscriptores y todos recibirán todos los datos. Si no hay suscriptores el dato se pierde.

Una "queue" es similar a un "topic" pero con un par de diferencias. El dato que se publica se mete en una cola de datos para esa queue. Si hay varios suscriptores, solo uno de ellos recibirá el dato. El siguiente dato puede recibirlo otro suscriptor, etc. Si no hay suscriptores, el dato queda en la cola y permanece hasta que alguien lo retire.

En nuestro ejemplo, instanciamos desde Spring dos JmsTemplate, uno para topic y otro para queue

    <bean name="jmsTopicTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="jmsConnectionFactory"/>
        <property name="pubSubDomain" value="true"/>
    </bean>

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

La única diferencia entre uno y otro es la property pubSubDomain que hemos puesto a true para el caso de topic. Por defecto está a false y entonces es para queue. Al crear este bean, hemos tenido que pasar también la connectionFactory que creamos en el punto anterior.


El Servidor[editar]

El broker de JMS[editar]

En el lado de nuestro servidor de ejemplo debemos levantar un servidor JMS (un broker). En spring se levanta de esta manera

    <amq:broker useJmx="false" persistent="false">
        <amq:transportConnectors>
            <amq:transportConnector uri="tcp://localhost:61616"/>
        </amq:transportConnectors>
    </amq:broker>

Este es propio de ActiveMq. los amq:broker y demás tags vienen del siguiente schema de xml

   http://activemq.apache.org/schema/core
   http://activemq.apache.org/schema/core/activemq-core.xsd

Con esto, cuando arranquemos nuestro servidor con Spring, tendremos levantado nuestro servidor (broker) JMS.

Enviar mensajes[editar]

Por supuesto, enviar mensajes puede hacerlo tanto el servidor como el cliente. Lo ponemos en este lado porque en nuestro ejemplo es el servidor el que va a enviar mensajes.

La clase de nuestro ejemplo que envía mensajes es JmsEmitter. Para instanciarla en el xml de spring ponemos

    <context:component-scan base-package="com.chuidiang.examples.spring_jms.server"/>

    <bean class="com.chuidiang.examples.spring_jms.server.JmsEmitter">
        <property name="topicTemplate" ref="jmsTopicTemplate"/>
        <property name="queueTemplate" ref="jmsQueueTemplate"/>
    </bean>

Veamos la clase y expliquemos tanto el XML como la clase java.

/**
 *
 * @author fjabellan
 * @date 28/11/2020
 */
@Slf4j
public class JmsEmitter {
    private int counter=0;

    @Getter @Setter
    private JmsTemplate topicTemplate;

    @Getter @Setter
    private JmsTemplate queueTemplate;

    /** Tras arrancar todo se llama a este metodo para que empiece a enviar por JMS */
    @PostConstruct
    public void init(){

        new Thread(new Runnable() {
            @SneakyThrows(InterruptedException.class)
            @Override
            public void run() {

                while (true) {
                    sendData();
                    Thread.sleep(1000);
                }

            }
        }).start();
    }

    private void sendData() {
        log.info("Envio "+counter);
        topicTemplate.convertAndSend(Constants.TOPIC, new Data(counter,"The Text "+counter));

        SonData sonData= new SonData(counter,counter,"The Text"+counter);
        sonData.setString("Son Data"+counter);
        sonData.setTheDouble(counter);
        sonData.setValue(counter);
        queueTemplate.convertAndSend(Constants.QUEUE,sonData);

        counter++;
    }
}

La clase tiene dos propiedades: topicTemplate y queueTemplete. En el fichero spring xml, instanciamos esta clase como un beam y le pasamos las dos instancias que tenemos de JmsTemplate para queue y para topic.

Si te fijas en la clase JmsEmitter, tiene un método init() anotado con @PostConstruct. Esta anotación es para que se llame a este método una vez esté todo arrancado. En este método se lanza un hilo para siempre jamás que se dedica a publicar datos usando los JmsTemplate. Los datos son las clases Data y SonData, que tienen usa serie de valores a rellenar y que implementan Serializable. Deben implementar Serializable porque si no, Spring/JMS no saben convertirlas en mensajes que puedan llegar a los consumidores.

Para que spring haga caso a esta anotación, en el spring xml tenemos que poner el ontext:component-scan que habrás visto en el trozo de xml donde instanciamos JmsEmitter.

Para el envío de mensajes se usan las clases jmsTemplate, en concreto los métodos convertAndSend(). Se pasa como primer parámetro un String que es el nombre que le queramos dar a la queue o al topic. Puede ser cualquiera siempre que servidor y cliente usen el mismo nombre. En nuestro ejemplo están en constantes de código, pero sus valores son simplemente "theTopic" y "theQueue".

El segundo parámetro es el objeto java que queremos enviar. Debe ser un objeto java que implemente Serializable. De esta forma Spring/JMS sabrán convertirlo a bytes par poder enviarlo a donde esté el cliente, quizás en otro ordenador, y allí reconstruirlo.

Listo, tenemos todo montado en el lado del servidor. Solo nos queda el main

Main del Servidor[editar]

El main del servidor es sencillo

/**
 * @author fjabellan
 * @date 28/11/2020
 */
public class JmsServerMain {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-server-context.xml");
    }
}

Simplemente crear un ClassPathXmlApplicationContext con nuestro fichero xml de spring donde hemos ido metiendo todo lo que hemos ido comentando. Al tener JmsEmitter un método anotado con @PostConstruct, se llamará a ese método cuando todo arranque y este se pondrá a publicar dato sen la queue y en el topic.

El Cliente[editar]

Listener de mensajes JMS[editar]

Comentamos que para recibir mensajes se podía usar la instancia de JmsTemplate. Esta instancia tiene métodos receive() o receiveAndConvert(). Son perfectamente válidos, pero requieren que tengamos un hilo pidiendo estos mensajes, puesto que la llamada a receive() puede quedarse bloqueada hasta que haya un mensaje disponible o hasta que pase un tiempo de espera.

Tenemos otro mecanismo que no requiere hacer esta llamada. Haríamos una clase "listener" que esté a la escucha de mensajes. Cuando llegue un mensaje, Spring/JMS llamará a nuestra clase pasándolo el mensaje y sólo tenemos que tratarlo.

En nuestro fichero spring xml lo pondríamos así

    <bean id="jmsTopicReceiver" class="com.chuidiang.examples.spring_jms.client.JmsTopicReceiver"/>
    <bean id="jmsQueueReceiver" class="com.chuidiang.examples.spring_jms.client.JmsQueueReceiver"/>

    <jms:listener-container connection-factory="jmsConnectionFactory"
                            destination-type="topic">
        <jms:listener destination="theTopic" ref="jmsTopicReceiver" method="receiveMessage"/>
    </jms:listener-container>

    <jms:listener-container connection-factory="jmsConnectionFactory">
        <jms:listener destination="theQueue" ref="jmsQueueReceiver" method="receiveMessage"/>
    </jms:listener-container>

Creamos dos clases, JmsTopicReceiver y JmsQueueReceiver que veremos ahora con más detalle, para recibir los mensajes de topic y queue respectivamente. Las instanciamos como bean de Spring en el fichero xml.

Hacemos dos bloques jms:listener-container, uno para topic y el otro para queue. Por defecto este bloque es para queue, así que en el que es para topic debemos poner destination-type como puedes ver en el bloque de xml. Dentro de cada uno de ellos ponemos uno o más jms:listener. Tenemos que indicar el nombre del topic (theTopic en nuestro caso) o de la queue (theQueue en nuestro caso), una referencia a la clase que va a tratar el mensaje (son las que hemos instanciado justo antes) y el nombre del método al que se debe llamar cuando llegue un mensaje (receiveMessage en nuestro caso).

Por supuesto, para tener disponibles jms: en tu xml de spring, debes poner en la cabecera los bloques

   http://www.springframework.org/schema/jms
   http://www.springframework.org/schema/jms/spring-jms.xsd

Ambas clases JmsTopicReceiver y JmsQueueReceiver son muy parecidas. Vamos a ver sólo una de ellas

@Slf4j
public class JmsTopicReceiver {
    public void receiveMessage(Object theData) {
        log.info("Recibido de topic " + theData);
    }
}

Básicamente tiene el método receiveMessage(Object) donde Object será el mensaje que recibimos. Aquí lo recibiremos tal cual como la clase java nuestra que hayamos enviado desde el servidor. Si recuerdas, la clase debía ser Serializable y Spring/JMS saben aquí recomponerla para dárnosla tal cual.

El main del cliente[editar]

Ya tenemos todo. Sólo queda el main del ciiente, que básicamente es instanciar el contexto de spring.

public class JmsGuiMain {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-gui-context.xml");
    }
}

Está listo. Si arrancamos servidor y cliente, ambos en el mismo PC puesto que hemos puesto "localhost" en la connection factory, deberíamos ver cómo el servidor envía un par de mensajes por segundo, uno a la queue el otro al topic, y como el cliente los va recibiendo y sacando por pantalla.