Compartir a través de


Guía de solución de problemas de Spring JMS

En este artículo se describe cómo solucionar problemas conocidos y errores comunes al usar Spring JMS. El artículo también responde a algunas preguntas más frecuentes sobre spring-cloud-azure-starter-servicebus-jms.

Problemas de conectividad

MessageProducer se cerró debido a un error irrecuperable

Descripción del problema

Al usar JmsTemplate para enviar mensajes, JmsTemplate deja de estar disponible durante un intervalo de inactividad comprendido entre 10 y 15 minutos. Enviar mensajes en ese intervalo puede obtener las excepciones que se muestran en la siguiente salida de ejemplo:

2022-11-06 11:12:05.762  INFO 25944 --- [   scheduling-1] c.e.demo.ServiceBusJMSMessageProducer    : Sending message: 2022-11-06T11:12:05.762072 message 1
2022-11-06 11:12:05.772 ERROR 25944 --- [   scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task

org.springframework.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.; nested exception is javax.jms.IllegalStateException: The MessageProducer was closed due to an unrecoverable error.
    at org.springframework.jms.support.JmsUtils.convertJmsAccessException(JmsUtils.java:274) ~[spring-jms-5.3.23.jar:5.3.23]
  ...
Caused by: org.apache.qpid.jms.provider.ProviderException: The link 'G0:36906660:qpid-jms:sender:azure:5caf3ef4-9602-413c-964d-cf1292d6e1f5:1:1:1:t4' is force detached. Code: publisher(link376). Details: AmqpMessagePublisher.IdleTimerExpired: Idle timeout: 00:10:00. [condition = amqp:link:detach-forced]
    at org.apache.qpid.jms.provider.amqp.AmqpSupport.convertToNonFatalException(AmqpSupport.java:181) ~[qpid-jms-client-0.53.0.jar:na]
  ...

Análisis de causa

Las excepciones se producen para azure Service Bus cuando la conexión amQP y el vínculo están activos, pero no hay llamadas ( por ejemplo, llamadas de envío o recepción) se realizan mediante el vínculo durante 10 minutos. En este caso, se cierra el vínculo. Y cuando se han cerrado todos los vínculos de la conexión porque no había ninguna actividad (inactiva) y no se ha creado un vínculo nuevo en 5 minutos, se cierra la conexión.

Para el inicio de JMS de Service Bus, el cachingConnectionFactory de se usa de forma predeterminada, que almacena en caché la sesión, el productor y el consumidor. Cuando el JmsProducer está inactivo durante más de 10 minutos, pero menos de 15, se ha cerrado el vínculo que ocupa el productor almacenado en caché. Los mensajes no se pueden enviar durante este intervalo. A continuación, después de otros 5 minutos inactivos, se cierra toda la conexión. Por lo tanto, cualquier operación de envío después del intervalo de inactividad de 15 minutos hace que el CachingConnectionFactory cree una nueva conexión para enviar. La operación de envío estará disponible después de 15 minutos.

Solución alternativa

Actualmente, el iniciador proporciona una solución alternativa para el problema de desasociación de vínculos aplicando el JmsPoolConnectionFactory, que agrupa Connection, Sessiony MessageProducer, y administra el ciclo de vida de las instancias agrupadas. Esta solución alternativa puede garantizar que un productor se expulse después de no estar disponible y, por tanto, todas las operaciones de envío se realizan en productores activos.

Para usar esta solución alternativa, agregue la siguiente configuración:

spring:
  jms:
    servicebus:
      pool:
        enabled: true
        max-connections: ${your-expected-max-connection-value}

Uso de spring.jms.servicebus.idle-timeout

Las propiedades de tiempo de espera de inactividad configuran el tiempo de espera de inactividad de una conexión AMQP. La especificación AMQP proporciona la siguiente descripción:

Las conexiones están sujetas a un umbral de tiempo de espera de inactividad. Un par local desencadena el tiempo de espera cuando no se reciben fotogramas después de superar un valor de umbral. El tiempo de espera de inactividad se mide en milisegundos y comienza desde la hora en que se recibe el último fotograma. Si se supera el umbral, un elemento del mismo nivel DEBE intentar cerrar correctamente la conexión mediante un marco de cierre con un error que explica por qué. Si el mismo nivel remoto no responde correctamente dentro de un umbral a este, el mismo nivel PUEDE cerrar el socket TCP.

En el caso de un cliente JMS, al configurar esta propiedad, controlará en el servidor cuánto tiempo espera que el servidor envíe un marco vacío para mantener activa una conexión cuando no se entrega ningún mensaje. Esta propiedad controla el comportamiento del mismo nivel remoto y cada elemento del mismo nivel puede tener su propio valor aislado.

Problemas de JmsTemplate

Mensajes programados

Azure Service Bus admite el procesamiento de mensajes retrasado. Para obtener más información, consulte la sección Mensajes programados de Secuenciación de mensajes y marcas de tiempo. Para JMS, para programar un mensaje, establezca la propiedad ScheduledEnqueueTimeUtc mediante el encabezado de anotación de mensaje x-opt-scheduled-enqueue-time.

Problemas de JmsListener

Se envían demasiadas solicitudes a Service Bus aunque no haya ningún mensaje en el servidor.

Descripción del problema

Al usar la API de @JmsListener, en algunos casos puede ver en Azure Portal que hay valores continuos para las solicitudes entrantes enviadas a su cola o temas, incluso si no hay mensajes en el servidor que se van a recibir.

Análisis de causa

@JmsListener es un agente de escucha de sondeo, que se crea para intentos de sondeo repetidos.

El agente de escucha se encuentra en un bucle de sondeo en curso. Cada bucle llama al método MessageConsumer.receive() de JMS para sondear al consumidor local para que los mensajes los consuman. De forma predeterminada, para cada operación de sondeo, el consumidor local envía solicitudes de incorporación de cambios al agente de mensajes para solicitar mensajes y, a continuación, bloquea durante un período de tiempo determinado. El proceso de sondeo concreto se decide mediante varias propiedades, como receiveTimeout, prefetchSizey receiveLocalOnly o receiveNoWaitLocalOnly. El método receiveNoWaitLocalOnly solo se usa cuando se establece receiveTimeout en un valor negativo.

Cuando se produzca este problema en la aplicación, compruebe las siguientes opciones de configuración:

  • Determine si la directiva de captura previa es 0, que también es la opción predeterminada. La captura previa 0 significa un consumidor de extracción que envía solicitudes de incorporación de cambios al Service Bus para cada sondeo.

  • Si ha configurado la captura previa no cero, determine si la propiedad receiveLocalOnly o receiveNoWaitLocalOnly está establecida en false, que es la opción predeterminada. Un valor de false aquí sigue enviando solicitudes de incorporación de cambios al servidor porque no solo sondea al consumidor local.

  • La configuración de receiveTimeout determina cuánto tiempo bloquea cada solicitud de incorporación de cambios, por lo que puede afectar a la frecuencia de las solicitudes de incorporación de cambios que se envían al servidor. El valor predeterminado es 1 segundo.

Para obtener un análisis completo, consulte la explicación del problema de GitHub .

Soluciones

En las secciones siguientes se describen dos soluciones para tratar este problema.

Solución 1. Cambio para insertar solo consumidor y comprobación local

Al cambiar el modo a push, el consumidor se convierte en un notificación asincrónica consumidor que no extrae mensajes del agente, pero mantiene una cantidad de crédito de vínculo objetivo. La cantidad se decide mediante una propiedad de captura previa. A medida que Service Bus (remitente) inserta mensajes, el crédito de vínculo del remitente disminuye y, cuando el crédito de vínculo del remitente está por debajo de un umbral, el cliente (receptor) envía una solicitud al servidor para aumentar el crédito de vínculo del remitente a la cantidad de destino deseada.

Para realizar esta solución, agregue la siguiente configuración:

En primer lugar, configure el número de prefetch como distinto de cero, que configura al consumidor como no extracción. En la tabla siguiente se muestran varias propiedades de captura previa, cada una de las cuales controla diferentes entidades de Service Bus. Establezca las propiedades que se aplican a su caso.

Propiedad Descripción
spring.jms.servicebus.prefetch.all El valor de reserva de la opción de captura previa en este espacio de nombres de Service Bus
spring.jms.servicebus.prefetch.queue-prefetch Número de captura previa de la cola.
spring.jms.servicebus.prefetch.queue-browser-prefetch Número de captura previa del explorador de colas.
spring.jms.servicebus.prefetch.topic-prefetch Número de captura previa del tema.
spring.jms.servicebus.prefetch.durable-topic-prefetch Número de captura previa del tema duradero.

En segundo lugar, configure el non-local-check agregando una clase de configuración para el personalizador de fábrica, como se muestra en el ejemplo siguiente:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    ServiceBusJmsConnectionFactoryCustomizer customizer() {
        return factory -> {
            factory.setReceiveLocalOnly(true);
            // Configure the below ReceiveNoWaitLocalOnly instead if you have specified the property 
            // spring.jms.listener.receive-timeout with negative value. Otherwise, configure the above `ReceiveLocalOnly`.
            //factory.setReceiveNoWaitLocalOnly(true);
        };
    }
}

El valor de captura previa puede afectar a la rapidez de envío de mensajes al búfer local del consumidor. Debe ajustar el valor según el rendimiento consumido y los volúmenes de mensajes. Un valor adecuado puede acelerar el proceso de consumo, mientras que un valor demasiado grande puede hacer que los mensajes almacenados en búfer local se vuelvan obsoletos y se envíen de nuevo. Para volúmenes de mensajes bajos, donde cada mensaje tarda mucho tiempo en procesarse, establezca la captura previa en 1. Este valor garantiza que un consumidor solo está procesando un mensaje cada vez.

Solución 2. Aumento del tiempo de espera de recepción para reducir la frecuencia de extracción

La propiedad de tiempo de espera de recepción determina la estrategia durante cuánto tiempo bloquea el consumidor para esperar un resultado de extracción. Por lo tanto, al extender el tiempo de espera, puede reducir la frecuencia de extracción y, a continuación, reducir el número de solicitudes de incorporación de cambios al elegir el modo de extracción. En casos extremos, puede establecer la estrategia para esperar indefinidamente hasta que llegue un mensaje, lo que significa que el consumidor solo extrae después de consumir un mensaje. En este caso, cuando no haya ningún mensaje en el servidor, se bloqueará para esperar.

Para realizar esta solución, configure la propiedad spring.jms.listener.receive-timeout. Esta propiedad es de tipo java.time.Duration y tiene un valor predeterminado de 1 segundo. En la lista siguiente se explica el efecto de varios valores:

  • Establecer el tiempo de espera de recepción en 0 significa que la extracción se bloquea indefinidamente hasta que se envía un mensaje.
  • Establecer el tiempo de espera de recepción en un valor positivo significa que la extracción se bloquea hasta el tiempo de espera del tiempo de espera.
  • Establecer el tiempo de espera de recepción en un valor negativo significa que la extracción es una recepción sin espera, lo que significa que devuelve un mensaje inmediatamente o null si no hay ningún mensaje disponible.

Nota

Un valor de tiempo de espera elevado puede aportar algunos efectos secundarios. Por ejemplo, un valor de tiempo de espera elevado también extenderá el tiempo que el subproceso principal está en un estado de bloque. Este estado significa que el contenedor tendrá menos capacidad de respuesta a las llamadas stop() y solo puede detenerse entre receive() llamadas.

Además, el contenedor solo puede enviar solicitudes después de que se haya superado el intervalo de receive-timeout. Si el intervalo es superior a 10 minutos, Service Bus cerrará el vínculo e impedirá que el agente de escucha envíe o reciba. Para más información, consulte la sección Link de errores de AMQP de en Azure Service Bus. De forma predeterminada, el agente de escucha usa un cachingConnectionFactory.

Si necesita un tiempo de espera de recepción elevado, asegúrese de usar el JmsPoolConnectionFactory.

Para obtener más información sobre el problema de cierre del vínculo y cómo usar JmsPoolConnectionFactory, vea la Se cerró MessageProducer debido a un error irrecuperable sección.

Problema de captura previa

Descripción del problema

Una directiva de captura previa no adecuada puede provocar los siguientes problemas:

  • Los mismos mensajes se consumen repetidamente.
  • Los mensajes se colocan en la cola de mensajes fallidos después de MaxDeliveryCountExceeded, incluso cuando los mensajes se procesan sin error o excepción.

Análisis de causa

Este problema suele ocurrir cuando el captura previa valor es mayor que la capacidad de consumo real, con el efecto de que se capturan previamente demasiados mensajes en el búfer local en espera de consumirse. Sin embargo, los mensajes capturados previamente se ven como enviados en un modo de de bloqueo de inspección desde el lado de Service Bus. Cada mensaje enviado tiene un max-delivery-count y atributos de duración de bloqueo. En el modo de recepción de bloqueo de inspección, los mensajes capturados en el búfer de captura previa se adquieren en el búfer en un estado bloqueado, con el reloj de tiempo de espera para el tic de bloqueo. Si el búfer de captura previa es grande y el procesamiento tarda tanto tiempo que los bloqueos de mensajes expiran mientras permanecen en el búfer de captura previa, el mensaje se trata como abandonado y se vuelve a poner a disposición para la recuperación de la cola.

Este problema puede hacer que el mensaje se capture en el búfer de captura previa y se coloque al final. Si el búfer de captura previa no se procesa antes de la expiración del mensaje, los mensajes se capturan previamente repetidamente, pero nunca se entregan de forma efectiva en un estado utilizable (bloqueado válidamente). A continuación, cuando esas copias obsoletas se quitan de la cola, la aplicación consume el mismo mensaje repetidamente y no puede completarlas. En otro caso, todos los mensajes repetidos expiran en el búfer antes de consumirse. En este caso, el mensaje de Service Bus se moverá finalmente a la cola de mensajes fallidos después de que se supere el número máximo de entregas.

Para obtener más información, consulte el ¿Por qué la captura previa no es la opción predeterminada? sección de mensajes de captura previa de mensajes de Azure Service Bus.

Solución

Tenga cuidado con la configuración de la captura previa para asegurarse de que se ajuste a la capacidad de consumo. Debe equilibrar el número máximo de capturas previas y la duración del bloqueo configurada en la cola o suscripción, de modo que el tiempo de espera de bloqueo al menos supere el tiempo de procesamiento de mensajes esperado acumulado para el tamaño máximo del búfer de captura previa, más un mensaje. Al mismo tiempo, el tiempo de espera de bloqueo no debe ser tan largo que los mensajes puedan superar su tiempo máximo de vida cuando se quitan accidentalmente, lo que requiere que su bloqueo expire antes de volver a entregarse.

Para configurar el atributo de captura previa, que tiene un valor predeterminado de cero, use una de las siguientes propiedades:

Propiedad Descripción
spring.jms.servicebus.prefetch.all Valor de reserva de la opción de captura previa en este espacio de nombres de Service Bus.
spring.jms.servicebus.prefetch.queue-prefetch Número de captura previa de la cola.
spring.jms.servicebus.prefetch.queue-browser-prefetch Número de captura previa del explorador de colas.
spring.jms.servicebus.prefetch.topic-prefetch Número de captura previa del tema.
spring.jms.servicebus.prefetch.durable-topic-prefetch Número de captura previa del tema duradero.

¿Cómo realizar la disposición de AMQP a Service Bus?

JMS admite cinco tipos de disposición de AMQP al reconocer mensajes al agente de mensajería. Los valores admitidos son ACCEPTED, REJECTED, RELEASED, MODIFIED_FAILEDy MODIFIED_FAILED_UNDELIVERABLE. Para más información, consulte la sección asignación de operaciones de AmQP y Service Bus de Uso de Java Message Service 1.1 con azure Service Bus estándar y AMQP 1.0.

Por lo tanto, para completar manualmente, abandonar, enviar, aplazar o liberar un mensaje mediante JmsListener, siga estos pasos:

  1. Deshabilite las transacciones de sesión y use el modo de confirmación de CLIENTE.

    Para realizar esta tarea, declare su propio JmsListenerContainerFactory bean y, a continuación, establezca las propiedades o publique el proceso de la JmsListenerContainerFactory definida en el starter. En el ejemplo siguiente se usa el enfoque de declarar otro bean:

    @Configuration(proxyBeanMethods = false)
    public class CustomJmsConfiguration {
    
        @Bean
        public JmsListenerContainerFactory<?> customQueueJmsListenerContainerFactory(
                DefaultJmsListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
            DefaultJmsListenerContainerFactory jmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
            configurer.configure(jmsListenerContainerFactory, connectionFactory);
            jmsListenerContainerFactory.setPubSubDomain(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionTransacted(Boolean.FALSE);
            jmsListenerContainerFactory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
            return jmsListenerContainerFactory;
        }
    }
    
  2. En el controlador de mensajes, complete o abandone mensajes explícitamente.

    @JmsListener(destination = "QUEUE_NAME", containerFactory = "customQueueJmsListenerContainerFactory")
    public void receiveMessage(JmsTextMessage message) throws Exception {
        String event = message.getBody(String.class);
        try {
            logger.info("Received event: {}", event);
            logger.info("Received message: {}", message);
            // by default complete the message
            message.acknowledge();
        } catch (Exception e) {
            logger.error("Exception while processing re-source event: " + event, e);
            JmsAcknowledgeCallback acknowledgeCallback = message.getAcknowledgeCallback();
            // explicitly abandon the message
            acknowledgeCallback.setAckType(MODIFIED_FAILED);
            message.setAcknowledgeCallback(acknowledgeCallback);
            message.acknowledge();
            throw e;
        }
    }
    

Problemas de configuración

Deshabilitación de la configuración automática de JMS de Service Bus

Descripción del problema

Algunos usuarios importan algunas instancias de Spring Cloud Azure Starter para la configuración automática de un servicio de Azure distinto de JMS de Service Bus. También usan el marco de Spring JMS sin necesidad de JMS de Service Bus. A continuación, cuando la aplicación intenta iniciarse, se producen las siguientes excepciones:

Caused by: java.lang.IllegalArgumentException: 'spring.jms.servicebus.connection-string' should be provided
    at com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties.afterPropertiesSet(AzureServiceBusJmsProperties.java:210)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800)
    ... 98 more

Análisis de causa

Este problema se produce porque todas las clases de configuración automática de Azure de Spring Cloud se colocan en el mismo módulo, por lo que cualquier spring Cloud Azure Starter importa realmente toda esa configuración automática, que también incluye JMS de Service Bus. A continuación, cuando la aplicación usa la API de Spring JMS, cumple la condición de configuración automática de JMS de Service Bus y la desencadena. A continuación, para los usuarios que no piensan usar spring-cloud-azure-starter-servicebus-jms, no se cumplen las condiciones de propiedad porque no hay ninguna razón para configurar Service Bus para JMS. Esta situación hace que se produzcan las excepciones.

Solución

Spring Cloud Azure for Service Bus JMS proporciona una propiedad para activar o desactivar su configuración automática. Puede optar por deshabilitar esta funcionalidad según sea necesario mediante el siguiente valor de propiedad:

spring.jms.servicebus.enabled=false

Configuración de atributos de mensaje

¿Cómo establecer el tipo de contenido de los mensajes salientes?

Para configurar el tipo de contenido, personalice el convertidor de mensajes para modificar el atributo de tipo de contenido al convertir mensajes. El código siguiente toma mensajes de bytes como ejemplo.

En primer lugar, personalice el convertidor de mensajes que se usará en el JmsTemplate, como se muestra en el ejemplo siguiente:

public class CustomMappingJackson2MessageConverter extends MappingJackson2MessageConverter {

    public static final String CONTENT_TYPE = "application/json";

    public CustomMappingJackson2MessageConverter() {
        this.setTargetType(MessageType.BYTES);
    }

    @Override
    protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter)
            throws JMSException, IOException {
        final BytesMessage message = super.mapToBytesMessage(object, session, objectWriter);
        JmsBytesMessage msg = (JmsBytesMessage) message;
        AmqpJmsMessageFacade facade = (AmqpJmsMessageFacade) msg.getFacade();
        facade.setContentType(Symbol.valueOf(CONTENT_TYPE));
        return msg;
    }
}

A continuación, declare el bean personalizado del convertidor de mensajes, como se muestra en este ejemplo:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter messageConverter() {
        return new CustomMappingJackson2MessageConverter();
    }
}

¿Cómo establecer el nombre de propiedad de identificador de tipo para MappingJackson2MessageConverter?

El atributo type-id-property-name permite al MappingJackson2MessageConverter determinar qué clase usar para deserializar la carga del mensaje. Al serializar cada objeto java en una carga de Spring Message, el convertidor almacena el tipo de carga en una propiedad de mensaje con el nombre de propiedad registrado por type-id-property-name. A continuación, al deserializar el mensaje, el convertidor lee el identificador de tipo del mensaje y realiza la deserialización.

Para establecer el type-id-property-name, declare su propio bean MappingJackson2MessageConverter y configure esa propiedad, como se muestra en el ejemplo siguiente:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

    @Bean
    public MessageConverter jacksonJmsMessageConverter()
    {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTypeIdPropertyName("your-custom-type-id-property-name");
        return converter;
    }
}

Detección de duplicados

Azure Service Bus admite detección de duplicados, que aplica la propiedad MessageId para identificar de forma única los mensajes y descartar los duplicados enviados a Service Bus.

Sin embargo, para la API de JMS, no debe establecer el identificador de mensaje de JMS, que se considera ilegales en las especificaciones de JMS. Por lo tanto, esta característica no se admite actualmente para Spring Cloud Azure Service Bus JMS Starter.

Para obtener más actualizaciones de esta característica, consulte el problema de gitHub .

Habilitación del registro de transporte de AMQP

Para obtener más información, consulte la sección habilitar el registro de transporte de AMQP de Solución de problemas de Service Bus.

Obtener ayuda adicional

Para obtener más información sobre las formas de ponerse en contacto con el soporte técnico, consulte soporte técnico en la raíz del repositorio.

Recursos para el inicio de JMS de Spring Cloud Azure Service Bus

Presentación de problemas de GitHub

Al presentar problemas de GitHub, se solicitan los detalles siguientes:

  • Configuración de Service Bus/Entorno de espacio de nombres
    • ¿Qué nivel es el espacio de nombres (estándar o premium)?
    • ¿Qué tipo de entidad de mensajería se usa (cola o tema)? y su configuración.
    • ¿Cuál es el tamaño medio de cada mensaje?
  • ¿Cuál es el patrón de tráfico? (es decir, el número de mensajes por minuto y si el cliente siempre está ocupado o tiene períodos de tráfico lentos).
  • Volver a reproducir código y pasos
    • Esto es importante, ya que a menudo no podemos reproducir el problema en nuestro entorno.
  • Trozas