viernes, 21 de abril de 2017

Introducción a la mensajería JMS

Introducción a la mensajería JMS

La especificación de mensajería JMS nos permite construir sistemas de mensajería asíncronos con un alto grado de robustez y sencillez. Además, hoy en día existen en el mercado soluciones JMS de código abierto que proporcionan un grado de madurez suficiente como para ser utilizados en aplicaciones corporativas.


La mensajería JMS


Comunicaciones asíncronas

Como es sabido, la comunicación asíncrona, también llamada "no bloqueante", es aquella en la que el emisor envía un mensaje y continúa con su funcionamiento normal sin esperar a que el receptor lo procese. El caso contrario es la comunicación síncrona en la que el emisor envía el mensaje y espera (se bloquea) hasta que recibe la respuesta o transcurre el tiempo de espera.

Los sistemas de mensajería (MOM, Message Oriented Middleware) se encargan de proporcionar este tipo de comunicación entre aplicaciones corporativas de una forma sencilla, robusta y fiable. Son mucho más ampliamente escalables que aquellos basados en conexiones directas o a través de llamadas a procedimientos remotos (RPC), y uno de los campos en los que son más utilizados es en el control de flujo (workflow) de información de sistemas.

En sistemas MOM los participantes de la comunicación no tienen que preocuparse de esperar una respuesta del recipiente, ni siquiera de dónde se encuentra éste, ya que pueden confiar en la infraestructura de mensajería que se encarga de asegurar su entrega.

JMS o Java Message Service, es la única API de mensajería soportada por J2EE.
Los sistemas de mensajería han ido evolucionando desde simple colas asíncronas hasta sistemas elaborados con publicadores, suscriptores, distribuidores, formateo de mensajes, capacidades de proporcionar calidad de servicio, etc.

Dentro de estos sistemas se conoce a los clientes de mensajería en JMS como clientes JMS, al propio sistema como proveedor JMS, y a la aplicación JMS como al conjunto de clientes y proveedores (normalmente uno) que forman el sistema.

El cliente JMS que produce el mensaje es conocido como productor y el que recibe se conoce como consumidor, aunque un mismo cliente JMS puede actuar a la vez de los dos modos.


Modelos estándar de mensajería

JMS nos proporciona dos modelos distintos de mensajería, publicación/subscripción y comunicación punto a punto mediante colas. Se suelen abreviar con "pub/sub" para el primer modelo y "p2p" para el segundo.

A grandes rasgos, el modelo publicación/subscripción está pensado para una comunicación "uno a muchos" mientras que el modelo punto a punto lo está para comunicaciones "uno a uno".
Podemos sugerir para el modelo pub/sub el servicio de subscripciones de una revista. Pensemos en que los lectores se apuntan a una lista mediante una subscripción y los responsables les envían su ejemplar cada mes. Todos y cada uno de ellos reciben una "copia" de la revista, y no es necesario que los lectores se conozcan entre sí.

Figura 1. Modelo pub/sub
Figura 1. Modelo pub/sub de mensajería JMS
Para el segundo modelo, podemos pensar en un sistema de colas típico, como puede ser las colas en las cajas de un supermercado. En este caso, los clientes buscan una sola de las cajas disponibles, hacen cola hasta que llega su turno y son atendidos por el/la cajero/a.

Figura 2. Modelo p2p
Figura 2
En las figuras 1 y 2 podemos apreciar el esquema de ambos modelos. En el modelo pub/sub un productor envía un mensaje a un canal virtual llamado tópico. Los consumidores pueden subscribirse a dicho tópico, con lo que recibirían una copia del mensaje; todos los mensajes enviados a un tópico son entregados a todos los receptores. En este modelo se conoce al productor como publicador y al consumidor como subscriptor. Un aspecto importante en este modelo es que el publicador no conoce nada acerca de los subscriptores, no sabe donde se encuentran, ni cuantos hay ni lo que hacen con los mensajes. Asímismo, los receptores no pueden examinar los mensajes pendientes, y tienen que consumirlo tal cual les llegan.

Los aspectos importantes de este modelo son los siguientes:
  1. No existe acoplamiento entre productores y consumidores, pueden ser añadidos dinámicamente.
  2. Cada subscriptor recibe su propia copia del mensaje
  3. Los subscriptores reciben el mensaje sin tener que solicitarlo. Los mensajes publicados en un tópico son automáticamente entregados a los subscriptores.

El modelo punto a punto se basa en otro esquema. Los clientes JMS envían mensajes a través de canales virtuales llamados colas. Aquí se conoce a los productores como emisores y a los consumidores como receptores. Se trata de un modelo en el cual los receptores chequean la cola para ver si han recibido algún mensaje, contrariamente a lo que sucedía en el anterior modelo (aunque es el comportamiento por defecto, se puede aproximar al modelo anterior mediante configuración).

En una cola puede haber más de un receptor esperando mensajes, aunque solamente uno de ellos va a consumir cada mensaje. Como observamos en la figura 2, el productor se encarga de generar el mensaje y el sistema JMS entrega el mensaje a uno y sólo uno de los potenciales receptores.

La especificación no define las reglas que deben seguirse para la distribución de los mensajes entre los receptores, así que cada fabricante realiza su propia implementación. Este modelo ofrece otras herramientas, como el explorador de colas mediante el cual el receptor es capaz de examinar los mensajes pendientes antes de consumirlos, de forma que puede descartar alguno de ellos. Esta es una característica diferenciadora del anterior modelo, además de las que explicamos a continuación:
  1. Los mensajes se intercambian a través de colas
  2. Cada mensaje se entrega a un solo receptor
  3. Los mensajes llegan ordenados, a medida que se consumen se van eliminando de la cola
  4. No existe acoplamiento entre emisores y receptores, se pueden añadir dinámicamente, ya que esta es una característica general de los sistemas de mensajería

El porqué de la existencia de ambos modelos tiene su explicación en los orígenes de la especificación JMS. Inicialmente se pensó como una solución para sustituir las APIs de los sistemas de mensajería existentes. En el momento del análisis, unos fabricantes de sistemas utilizaban un modelo y el resto el otro modelo. Así pues, JMS tuvo que dar opción a ambos modelos para que la industria lo aceptase. En realidad la especificación no exige que las implementaciones proporcionen los dos, aunque los proveedores de JMS lo ofrecen.

Fundamentalmente, todo lo que se puede hacer con un modelo también se puede hacer con el otro. Podemos establecer una analogía en relación a qué lenguaje de programación preferimos, sea el que sea, seguro que podremos conseguir el mismo resultado. De la misma forma, la elección del modelo pub/sub o p2p se convierte en una cuestión de preferencias.

Ante la existencia de los dos modelos, surge la duda de cuándo elegir uno u otro. La decisión va a depender de los distintos méritos que aporta cada uno. Si se trata de una aplicación en la que nos interesa repartir mensajes a distintos destinatarios sin importarnos si están conectados o no, el modelo pub/sub puede servirnos. Si por el contrario es importante saber que los mensajes llegan, como puede ser el caso de una conversación uno a uno, quizás sea más interesante utilizar el modelo p2p.

La variedad de los datos a transmitir también puede ser un punto a tener en cuenta. Podemos aprovecharnos de la facilidad de tópicos que nos ofrece el modelo pub/sub para segregar los diferentes mensajes entre los potenciales destinatarios.

El modelo p2p es más adecuado cuando se quiere que el receptor procese el mensaje una sola vez. Otra ventaja, mencionada anteriormente, es que disponemos de un explorardor de colas que nos permite echar una ojeada a la cola para ver los mensajes que esperan ser consumidos.

OpenJMS. Instalación y configuración

OpenJMS es una implementación libre de la especificación Java Messages Services API 1.0.2. La pagina del proyecto es: http://openjms.sourceforge.net.

Instalación

La instalación de OpenJMS es muy sencilla, únicamente necesitaremos tener instalado previamente el JRE (en su caso el J2SDK si deseamos modificar el código fuente del proyecto) y seguir unos sencillos pasos.

El archivo de instalación es únicamente una estructura de directorios que contiene todo lo necesario para ejecutar openJMS en nuestra maquina. El primer paso consiste en descomprimir el archivo de instalación que está disponible en formato .zip y .tar.gz. La estructura de directorios generada al descomprimir el archivo debe ser la siguiente:

-bin
-config
+--db
+--examples
-docs
-lib
-src
+--examples

La carpeta bin contiene archivos .sh y .bat para iniciar, detener, y administrar en servidor OpenJMS. La carpeta config contiene el archivo openjms.xml el cual indica la configuración por omisión del servidor OpenJMS. La carpeta config/db contiene scripts SQL para bases de datos OpenJMS.

La carpeta config/examples contiene varios ejemplos de archivos de configuración para otras necesidades de funcionamiento del servidor. La carpeta docs contiene toda la documentación del proyecto, incluyendo información más detallada de esta instalación. La carpeta lib contiene los archivos .jar requeridos para ejecutar el servidor OpenJMS y aquellos requeridos por programas cliente que usan OpenJMS. La carpeta src/examples contiene el código fuente de varios archivos de ejemplo.

Además de esto, hay que crear las siguiente variables de entorno:

JAVA_HOME - El directorio raíz de instalación del JRE.
OPENJMS_HOME - El directorio raíz de instalación de OpenJMS.

Para probar si la instalación se realizó satisfactoriamente iniciamos el servidor, para ello ejecutamos lo siguiente en la línea de comandos:

Para Windows:

cd %OPENJMS_HOME%\bin
startup


Para UNIX:
cd $OPENJMS_HOME/bin
startup.sh

Figura 3. Consola de OpenJMS

Figura 3

Configuración

La configuración de OpenJMS se realiza a través de la modificación del archivo openjms.xml. Simplemente se van agregando los elementos de configuración que necesitemos para el entorno sobre el cual ejecutaremos el servidor; dichos elementos pueden ser para configurar la bases de datos que vamos a utilizar, la seguridad, configuración de tópicos para publicación/suscripción y más opciones. Como ejemplo vemos en el listado 1 el archivo de configuración utilizado para ejecutar el ejemplo que acompaña a este artículo.
Listado 1. Ejemplo de configuración de OpenJMS
<?xml version="1.0"?>
<!--
     NOTA: Esta configuracion muestra los elementos más relevantes cuando se utiliza un conector RMI.
-->
<Configuration>
  <!-- Opcional. Representa la configuracion por omision-->
<ServerConfiguration host="localhost" embeddedJNDI="true" />
  
<!-- Requerido cuando se usa un conector RMI -->
<Connectors>
    <Connector scheme="rmi">
      <ConnectionFactories>
        <QueueConnectionFactory name="JmsQueueConnectionFactory" />
        <TopicConnectionFactory name="JmsTopicConnectionFactory" />
      </ConnectionFactories>
    </Connector>
  </Connectors>
    
  <!-- Requerido -->
  <DatabaseConfiguration>
    <JdbmDatabaseConfiguration name="openjms.db" />
  </DatabaseConfiguration>

  <!-- Requerido -->    
  <AdminConfiguration script="${openjms.home}\bin\startup.bat" />
    
<!-- Opcional. Si no se especifica, no se crearan destinos -->
<AdministeredDestinations>
    <AdministeredTopic name="charla">
      <Subscriber name="sub1" />
      <Subscriber name="sub2" />
</AdministeredTopic>
  
    <AdministeredQueue name="queue1" />
    <AdministeredQueue name="queue2" />
    <AdministeredQueue name="queue3" />
  </AdministeredDestinations>

  <!-- Opcional. Si no se especifica, no se crearan usuarios-->
<Users>
    <User name="admin" password="openjms" /> 
</Users>
</Configuration>


Podemos observar las instrucciones más relevantes para una configuración con un conector RMI, que es la que viene con OpenJMS por omisión al momento de instalarlo. Los posibles elementos de configuración de este archivo se pueden ver a continuación en la siguiente tabla.

Tabla 1. Opciones de configuración del servidor JMS

Tabla_1

ConfiguraciónDescripción
JDBCOpenJMS se puede configurar para usar bases de datos JDBC para implementar persistencia de mensajes.
Conectores OpenJMSOpenJMS proporciona opciones de conectividad sobre varios protocolos, utilizando conectores
JNDIOpenJMS utiliza JNDI para hacer disponibles al cliente: fábricas de conexiones, tópicos, y colas.
Fábricas de conexiónOpenJMS permite configurar la fábrica de conexiones con distintas opciones.
SeguridadOpenJMS proporciona mecanismos para implementar autenticación de conexiones
DestinosLos destinos son registrados con JNDI por el servidor OpenJMS para que estén disponibles a los clientes.
Garbage CollectionOpenJMS permite configurar a detalle la manera en que se ejecutará el recolector de basura.


A continuación se muestran algunos ejemplos de estos elementos de configuración. En el listado 2 se muestra un ejemplo de configuración JDBC:

Listado 2. Ejemplo de Configuración JDBC

  <DatabaseConfiguration>
    <RdbmsDatabaseConfiguration
      driver="oracle.jdbc.driver.OracleDriver"
      url="jdbc:oracle:oci8:@myhost" 
      user="openjms" 
      password="openjms" />
  </DatabaseConfiguration>


Actualmente OpenJMS esta configurado para ser compatible con JDBC 2.0 y varios sistemas de bases de datos. Además de agregar este fragmento de código a nuestro archivo de configuración, es necesario agregar al classpath la ruta de nuestro driver JDBC.

OpenJMS proporciona conectividad a través de varios protocolos utilizando conectores. El listado 3 muestra un ejemplo de configuración para habilitar un conector RMI:

Listado 3. Ejemplo de configuración de un conector RMI
<Connectors>
    <Connector scheme="rmi">
      <ConnectionFactories>
        <QueueConnectionFactory name="QueueConnectionFactory"/>
        <TopicConnectionFactory name="TopicConnectionFactory"/>
      </ConnectionFactories>
    </Connector>
</Connectors>

Los conectores soportados actualmente por OpenJMS son: RMI, TCP, TCPS, HTTP, HTTPS, Embedded.
Una ventaja importante de OpenJMS es que nos permite definir múltiples conectores a nuestro servidor.

OpenJMS proporciona configuraciones para realizar autenticación de conexiones. Para ello se utilizan 2 elementos de configuración <SecurityConfiguration> y <Users>. El listado 4 muestra como utilizar dichos elementos para habilitar la autenticación de conexiones.

Listado 4. Ejemplo de configuración para habilitar autenticación de conexiones.

<SecurityConfiguration securityEnabled="true"/>

<Users>
    <User name="admin" password="openjms"/>
    <User name="user1" password="password1"/>
    <User name="user2" password="password2"/>
</Users>


Como podemos observar, OpenJMS proporciona una amplia gama de características de configuración, lo que lo convierten en un servidor JMS muy versátil. La finalidad de este apartado es mostrarte un panorama general de las opciones de configuración de OpenJMS. Para conocer con mayor detalle la información para la configuración, puedes recurrir a la pagina del proyecto en internet.


Ejemplo con el modelo pub/sub y OpenJMS

Como ejemplo de aplicación JMS ofrecemos el típico chat. Se trata de una sola clase java ClienteJMSChat.java, que actúa tanto como suscriptor como publicador de un tópico de mensajes JMS. Por tanto, se trata de un ejemplo del modelo de publicación/suscripción.

Hay que decir que el ejemplo aquí propuesto es realmente exagerado, el hecho de utilizar un sistema robusto como JMS para una aplicación de chat es quizá excesivo, pero sirva como ilustración.

Los métodos de nuestra clase son los siguientes:
  1. main(). Conocido método principal para hacer que la clase sea ejecutable.
  2. initialize(String, String, String). Método de instancia para la inicialización de la clase como su propio nombre indica. Veremos su contenido a continuación.
  3. show(). Muestra el mensaje recibido desde el sistema JMS
  4. debug(). Método para ver mensajes de depuración. Separado de show() por conveniencia, aunque realmente hacen lo mismo.
  5. chatIt(String). Envia un mensaje al tópico JMS
  6. onMessage(Message). Método que debe implementar la clase para recibir mensajes JMS.
  7. close(). Cierra la conexión JMS abierta.

En la ejecución del ejemplo, el programa nos va a ir sacando trazas por pantalla, que ayudarán a comprenderlo mejor.


La función main()

Pasemos a examinar la función principal main(). En primer lugar observamos que salta una excepción si el número de argumentos pasados es inferior a tres. Los argumentos que hay que pasar son: el primero el tópico al cual queremos conectar, el segundo el nombre de usuario que vamos a utilizar y el tercero la contraseña del usuario (obviamente no se permiten en este caso contraseñas en blanco). Más adelante veremos cómo hay que configurar el servidor JMS para esté disponible el tópico para el cliente.
A continuación instanciamos un objeto de la clase ClienteJMSChat y la inicializamos con los argumentos anteriores. Una vez hecho esto obtenemos el stream de la entrada estándar para poder leer los mensajes que el usuario escribe. Todo texto que escriba el usuario es pasado al objeto instanciado llamando al método chatIt() excepto en el caso de que se corresponda con el comando de salida EXIT_COMMAND (también configurable mediante una variable estática) que hará que el programa termine. Si durante la comunicación ocurre un problema en la capa JMS la capturaremos con la excepción correspondiente.

En el listado 5 podemos observar la parte principal de este método:

Listado 5.

ClienteJMSChat chat = new ClienteJMSChat();
//Argumentos: topic - nombre de usuario - contraseña
chat.initialize(args[0], args[1], args[2]);

//Leer los mensajes desde la consola
BufferedReader consola = new java.io.BufferedReader(
new InputStreamReader(System.in));

//Bucle hasta que se introduzca el comando de finalización
while (true) {
    String s = consola.readLine();
    try {
        if (s.equalsIgnoreCase(EXIT_COMMAND)) {
            chat.close(); //Cerrar la conexión
            break;
        } else {
            chat.chatIt(s);
        }
    } catch (JMSException jmse) {
        chat.debug("Excepcion JMS: " + jmse.getMessage());
        break;
    }
}

Inicialización JMS

Los pasos para inicializar nuestro programa JMS son los siguientes:
  1. Inicializar JNDI
  2. Obtener el objeto Factory JMS
  3. Crear una conexión al servidor JMS mediante el objeto Factory
  4. Obtener dos sesiones JMS, una para publicar y otra para suscribirse al tópico (recordar que nuestro programa actúa con ambos roles)
  5. Obtener el objeto tópico a través de JNDI
  6. Por cada sesión, obtener un objeto publicador y suscriptor respectivamente, que actúan como agentes de comunicación
  7. Registrar en el suscriptor la clase actual para que reciba los mensajes que se publican en el tópico
  8. Por último, realizar la conexión con el servidor

En el listado 6 podemos ver estos pasos. Las variables que no aparecen declaradas son variables de clase, se puede consultar el código completo para ver su declaración. En concreto y para conectar con JMS, los datos que hay que proporcionar son:
  1. CONTEXT_FACTORY = "org.exolab.jms.jndi.InitialContextFactory"
  2. PROVIDER_URL = "rmi://localhost:1099/" (suponiendo que se conecta al host local)
  3. TOPIC_CONNECTION_FACTORY = "JmsTopicConnectionFactory"

Listado 6. Inicialización

//Obtener una conexión JNDI para acceder a los objetos JMS
Properties properties = new Properties();

//Propiedades específicas para conectar con OpenJMS
properties.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY);
properties.put(Context.PROVIDER_URL, PROVIDER_URL);

try {
    //Obtener contexto inicial JNDI
    InitialContext context = new InitialContext(properties);

    //Obtener la factoría JMS
    TopicConnectionFactory conFactory = (TopicConnectionFactory) context
                    .lookup(TOPIC_CONNECTION_FACTORY);

    //Crear la conexión
    TopicConnection conexion = conFactory.createTopicConnection(
                    usuario, password);

    //Creamos dos sesiones, una para publicación, donde se envian los 
    // mensajes que escribimos, y otra para suscribirnos al tópico, de
    // forma que recibamos los mensajes que allí son enviados.
    TopicSession sesionP = conexion.createTopicSession(false,
                    Session.AUTO_ACKNOWLEDGE);
    TopicSession sesionS = conexion.createTopicSession(false,
                    Session.AUTO_ACKNOWLEDGE);

    //Obtener el tópico JMS
    Topic topicObj = (Topic) context.lookup(topico);

    //Crear un publicador y un suscriptor JMS
    TopicSubscriber suscriptor = sesionS.createSubscriber(topicObj);
    TopicPublisher publicador = sesionP.createPublisher(topicObj);

    //Esta misma clase es la que recibe los mensajes
    suscriptor.setMessageListener(this);

    ...

    //Iniciar la conexion, a partir de aqui los mensajes pueden ser
    // enviados al tópico
    conexion.start();

} catch (NamingException ne) {
    throw new RuntimeException("Error JNDI", ne);

} catch (JMSException jmse) {
    throw new RuntimeException("Error JMS", jmse);
}

Envío y recepción de mensajes

Como nuestra clase implementa el interface javax.jms.MessageListener, debemos crear el método onMessage() que recibe los mensajes que provienen del sistema JMS cuando algún cliente envía un mensaje al tópico. Según vemos en la implementación, el método recibe un mensaje tipo Message, se convierte a un mensaje de tipo texto y obtenemos el texto como cadena para mostrarlo en la consola.

Listado 7.

try {
    TextMessage textMessage = (TextMessage) message;
    String text = textMessage.getText();
    show(text);
} catch (JMSException jmse) {
    jmse.printStackTrace(System.out);
}

Por último examinamos cómo se envian los mensajes al tópico. Simplemente creamos un mensaje tipo texto desde el objeto sesión que corresponde al publicador y establecemos la cadena del mensaje que queremos enviar. A continuación utilizamos el objeto publicador para enviar el mensaje al tópico, como vemos en el listado 8.

Listado 8.

TextMessage message = sesionP.createTextMessage();
message.setText("[" +usuario + "] " + text);
publicador.publish(message);

En el código que acompaña a este artículo podemos observar que en los imports no se hace referencia a ninguna clase específica de JMS, únicamente al obtener el contexto inicial JNDI es donde especificamos datos propios de OpenJMS, pero incluso esto lo hemos aislado en una variable estática, el código fácilmente puede ser adaptado para que lea dichos datos de un archivo .properties externo, por ejemplo.

Figura 4. Ejecución del ejemplo en Eclipse


Figura 4
Para compilar y ejecutar el ejemplo, además del fuente necesitaremos incluir la API JMS y la implementación OpenJMS para el cliente. Los archivos .jar que vamos a necesitar son los siguientes:
  1. jms-1.0.2a.jar. API JMS
  2. openjms-client-0.7.6.1.jar. Implementación para clientes OpenJMS

Ejecutar el ejemplo

Para ejecutar el ejemplo, ofrecemos tres opciones:
  1. Importar el proyecto en el IDE Eclipse
  2. Utilizar Ant para compilar, crear el javadoc y ejecutarlo
  3. Utilizar un script .bat (.sh para Linux) para ejecutarlo
Para importar el proyecto a Eclipse, utilizaremos la opción “Import” del menú “File”, y especificaremos que se trata de un proyecto externo. También podemos copiar el directorio dentro del workspace de Eclipse, y crear un nuevo proyecto Java con el mismo nombre, para que sea reconocido dentro del editor.
Para aquellos lectores familiarizados con Ant (http://ant.apache.org) el ejemplo viene con el script correspondiente con el que podremos compilar, ejecutar el ejemplo e incluso crear el javadoc.
Por último, para hacerlo más sencillo, proporcionamos los scripts para la ejecución del ejemplo desde consola: run.bat (Windows) y run.sh (Linux). Deberemos proporcionar los parámetros necesarios a estos scripts, según se ha explicado.

Conclusiones

Hasta la llegada de JMS, cada proveedor de mensajería definía su propia API. Mientras que cada solución tenía sus propios protocolos, la similitud lógica entre las distintas versiones eran las mismas. Esto hizo posible la existencia de JMS para estandarizar de alguna forma el desarrollo de sistemas de mensajería.

JMS se convierte en una API simple y a la vez robusta, para la implementación en nuestras aplicaciones de este tipo de sistemas, con la ventaja de que existen soluciones Open Source ya maduras como OpenJMS que podemos utilizar de una forma productiva.
Blogger Widgets