Spring JMS 疑難解答指南

本文說明如何在使用 Spring JMS 時針對已知問題和常見錯誤進行疑難解答。 本文也會回答 spring-cloud-azure-starter-servicebus-jms的一些常見問題。

連線問題

MessageProducer 因為無法復原的錯誤而關閉

問題描述

使用 JmsTemplate 傳送訊息時,JmsTemplate 會在 10 到 15 分鐘的閒置間隔期間變成無法使用。 以該間隔傳送訊息可以取得下列範例輸出中顯示的例外狀況:

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]
  ...

原因分析

當 AMQP 連線和連結處於作用中狀態,但有 10 分鐘未透過該連結進行任何呼叫(例如傳送或接收呼叫)時,Azure 服務匯流排 會發生例外情況。 在此情況下,鏈接會關閉。 當連線中的所有連結都因沒有活動而關閉(閒置),且在 5 分鐘內未建立新的連結時,該連線就會關閉。

針對服務總線 JMS 入門版,預設會使用 CachingConnectionFactory,以快取會話、產生者和取用者。 當 JmsProducer 閑置超過 10 分鐘,但少於 15 分鐘時,快取產生者佔用的連結已關閉。 在此間隔內無法傳送訊息。 然後,在另外 5 分鐘閑置之後,整個連線就會關閉。 因此,在15分鐘閒置間隔之後的任何傳送作業都會造成 CachingConnectionFactory 建立要傳送的新連線。 傳送作業會在 15 分鐘後變成可用。

因應措施

目前,starter 會透過套用 JmsPoolConnectionFactory 來為 link-detach 問題提供因應措施;它會將 Connection、Session 和 MessageProducer 納入集區,並管理這些集區執行個體的生命週期。 此權宜作法可確保 producer 在變得無法使用後會被移除,因此所有傳送作業都會由活動中的 producer 執行。

若要使用此因應措施,請新增下列設定:

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

spring.jms.servicebus.idle-timeout 的用途

閒置逾時屬性會設定 AMQP 連線的 閒置逾時。 AMQP 規格提供下列描述:

聯機受限於閑置逾時閾值。 當超過門檻值後仍未收到任何訊框時,逾時會由本機端對等體觸發。 閒置逾時時間以毫秒為單位計算,並從收到最後一個訊框時開始計時。 如果超過閾值,則對等端應嘗試使用附帶說明原因之錯誤訊息的關閉訊框,以正常方式關閉連線。 如果遠端對等端未在對此的門檻時間內以正常方式回應,則對等端可以關閉 TCP 套接字。

若為 JMS 用戶端,當您設定此屬性時,您可以在伺服器端控制您希望伺服器傳送空白框架的時間,以在未傳遞訊息時讓連線保持運作。 此屬性會控制遠端對等端的行為,而且每個對等端都可以有各自獨立的值。

JmsTemplate 問題

已排程的訊息

Azure 服務總線支援延遲的訊息處理。 如需詳細資訊,請參閱 訊息排序和時間戳的 排程訊息 一節。 若使用 JMS,要排程訊息,請使用訊息批註標頭 x-opt-scheduled-enqueue-time 來設定 ScheduledEnqueueTimeUtc 屬性。

JmsListener 問題

即使伺服器中沒有訊息,仍會將太多要求傳送至服務總線

問題描述

使用 @JmsListener API 時,在某些情況下,即使服務端沒有可接收的訊息,您仍可能會在 Azure 入口網站中看到,傳送到其佇列或主題的傳入要求數值持續存在。

原因分析

@JmsListener 是輪詢接聽程式,這是針對重複輪詢嘗試而建置的。

監聽器運作於持續輪詢迴圈中。 每個迴圈都會呼叫 JMS MessageConsumer.receive() 方法來輪詢本機取用者以取得要取用的訊息。 預設情況下,每次輪詢作業時,本機消費者都會將拉取請求傳送至訊息代理程式以取得訊息,然後阻塞一段時間。 具體輪詢程式是由數個屬性決定,包括 receiveTimeout、prefetchSize和 receiveLocalOnly 或 receiveNoWaitLocalOnly。 只有當您將 receiveNoWaitLocalOnly 設為負值時,才會使用 receiveTimeout 方法。

當您的應用程式發生此問題時,請檢查下列組態設定:

  • 判斷您的預先擷取原則是否為0,這也是預設選項。 預先擷取值為 0,表示提取式取用者每次輪詢時,都會向 服務匯流排 傳送提取要求。

  • 如果您已設定非零預先擷取,請判斷您的 receiveLocalOnly 或 receiveNoWaitLocalOnly 屬性是否設定為 false,這是預設選項。 此處的 false 值仍然會導致將提取要求傳送至伺服器,因為它不只會輪詢本機取用者。

  • receiveTimeout 組態會決定它針對每個提取要求封鎖的時間長度,因此可能會影響傳送至伺服器的提取要求頻率。 預設值為 1 秒。

如需完整的分析,請參閱 GitHub 問題中的討論。

解決方案

下列各節說明處理此問題的兩個解決方案

解決方案 1. 變更為僅推送用戶端和本機檢查

藉由將模式變更為 push,取用者會變成 異步通知 取用者,而該取用者不會從訊息代理程式提取訊息,但會維護目標連結點數。 其數量由預先擷取屬性決定。 當服務匯流排(發送端)推送訊息時,發送者的連結信用會減少,當發送者的連結信用低於某個門檻時,客戶端(接收端)會向伺服器發送請求,將發送者的連結信用點數增加至期望的目標數值。

若要完成此解決方案,請新增下列組態:

首先,將 prefetch 設為非零值,這會將消費者設定為非拉取模式。 下表顯示數個預先擷取屬性,每個屬性都會控制不同的服務總線實體。 設定適用於您的案件的屬性。

財產 描述
spring.jms.servicebus.prefetch.all 此 服務匯流排 命名空間中預擷取選項的後備值
spring.jms.servicebus.prefetch.queue-prefetch 佇列的預先擷取號碼。
spring.jms.servicebus.prefetch.queue-browser-prefetch 佇列瀏覽器的預先擷取編號。
spring.jms.servicebus.prefetch.topic-prefetch 主題的預先擷取編號。
spring.jms.servicebus.prefetch.durable-topic-prefetch 長期主題的預先擷取編號。

其次,請為工廠自訂器新增組態類別來設定 non-local-check,如以下範例所示:

@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);
        };
    }
}

預先擷取值可能會影響將訊息分派至取用者的本機緩衝區的速度。 您應該根據取用效能和訊息量來調整該值。 適當的值可以加快訊息取用流程,而過大的值可能會導致本機緩衝的訊息變得過時,並再次被分派。 對於訊息量較低,且每則訊息都需要較長時間處理的情況,請將預先擷取數設為 1。 此值可確保取用者一次只會處理一則訊息。

解決方案 2. 增加接收逾時時間以降低拉取頻率

接收逾時屬性會決定取用者封鎖等候提取結果多久的策略。 因此,藉由延長逾時時間,您可以降低拉取頻率,進而在選擇拉取模式時減少拉取請求的次數。 在極端情況下,您可以設定在訊息送達之前無限期等候的策略,這表示取用者只會在取用訊息之後提取。 在此情況下,當伺服器中沒有任何訊息時,它會進入阻塞狀態等待。

若要完成此解決方案,請設定 spring.jms.listener.receive-timeout 屬性。 此屬性的類型為 java.time.Duration,預設值為 1 秒。 下列清單說明各種值的效果:

  • 將接收逾時設定為 0 表示提取區塊無限期,直到分派訊息為止。
  • 將 receive-timeout 設為正值,表示 pull 作業最多會封鎖到逾時時間長度。
  • 將接收逾時設為負值,表示提取作業為非等待接收,也就是說,它會立即傳回訊息;如果沒有可用訊息,則立即傳回 null。

注意

較長的逾時值可能會帶來一些副作用。 例如,高逾時值也會延長主線程處於區塊狀態的時間。 此狀態表示容器不會回應 stop() 呼叫,而且只能在 receive() 呼叫之間停止。

此外,容器只能在傳遞 receive-timeout 間隔之後傳送要求。 如果間隔超過 10 分鐘,服務匯流排 將關閉連線,並使接聽器無法傳送或接收。 如需詳細資訊,請參閱 Azure 服務匯流排中的 AMQP 錯誤中的 連結已關閉一節。 根據預設,接聽程式會使用 CachingConnectionFactory。

如果您需要較長的接收逾時時間,請務必使用 JmsPoolConnectionFactory。

如需鏈接關閉問題及如何使用 JmsPoolConnectionFactory的詳細資訊,請參閱 MessageProducer 因無法復原的錯誤而關閉 一節。

預先擷取問題

問題描述

不當的預先擷取原則可能導致下列問題:

  • 相同的訊息被重複消費。
  • 即使訊息在處理過程中未發生錯誤或例外狀況,仍會在 MaxDeliveryCountExceeded 之後放入死信佇列。

原因分析

此問題通常發生在值 prefetch 高於實際消耗容量時,導致太多訊息被預先取入本地緩衝區等待被消耗。 然而,從 服務匯流排 端來看,預先擷取的訊息會被視為已在 peek-lock 模式中傳送。 每個發送的訊息都有 a max-delivery-count 和 lock-duration 屬性。 在 peek-lock 接收模式下,擷取到預取緩衝區的訊息會以鎖定狀態被接收到緩衝區,鎖的逾時時鐘仍在持續。 如果預先擷取緩衝區很大,且處理時間長到使訊息在停留於預先擷取緩衝區期間其鎖定已過期,則該訊息會被視為已放棄,並再次可從佇列擷取。

此問題可能會導致訊息被擷取到預取緩衝區中,並被放到最後面。 如果在訊息到期前未處理預先擷取緩衝區,則會重複預先擷取訊息,但從未以可用(有效鎖定)狀態有效傳遞訊息。 然後,當那些已過時的副本從佇列中取出時,應用程式就會反覆處理同一則訊息,而且無法完成這些訊息的處理。 在另一種情況下,重複訊息都在被取用前於緩衝區中過期。 在此情況下,服務匯流排 中的訊息在超過最大傳遞次數後,最終會移至死信佇列。

如需詳細資訊,請參閱 為什麼預先擷取不是默認選項?預先擷取 Azure 服務總線訊息的 區段。

解決方案

請謹慎設定預先擷取,以確保其符合取用端的處理能力。 您必須平衡佇列或訂用帳戶上設定的最大預先擷取計數和鎖定持續時間,讓鎖定逾時至少超過預先擷取緩衝區大小上限的累計預期訊息處理時間,加上一則訊息。 同時,鎖定逾時時間不應過長,以免訊息在意外遭到捨棄時,因必須等到鎖定到期後才能重新傳遞,而超過其最長存留時間。

若要設定具有預設值為零的預先擷取屬性,請使用下列其中一個屬性:

財產 描述
spring.jms.servicebus.prefetch.all 此服務總線命名空間中預先擷取選項的後援值。
spring.jms.servicebus.prefetch.queue-prefetch 佇列的預先擷取號碼。
spring.jms.servicebus.prefetch.queue-browser-prefetch 佇列瀏覽器的預先擷取編號。
spring.jms.servicebus.prefetch.topic-prefetch 主題的預先擷取編號。
spring.jms.servicebus.prefetch.durable-topic-prefetch 長期主題的預先擷取編號。

如何對服務總線執行AMQP處置?

JMS 在向訊息代理程式認可訊息時,支援五種 AMQP 處置類型。 支援的值為 ACCEPTED、REJECTED、RELEASED、MODIFIED_FAILED和 MODIFIED_FAILED_UNDELIVERABLE。 如需詳細資訊,請參閱 搭配 Azure 服務匯流排標準和 AMQP 1.0 使用 Java Message Service 1.1 中的 AMQP 處置和服務匯流排作業對應 一節。

因此,若要使用 JmsListener 手動完成、放棄、將訊息移至死信佇列、延期處理或釋出訊息,請依照下列步驟:

  1. 停用會話交易並使用 CLIENTack 模式。

    若要完成這項工作,您可以宣告自己的 JmsListenerContainerFactory Bean,然後設定其屬性;或者對 starter 中定義的 JmsListenerContainerFactory 進行後處理。 下列範例會使用宣告另一個豆子的方法:

    @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. 在您的訊息處理程式中,明確完成或放棄訊息。

    @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;
        }
    }
    

設定問題

停用 服務匯流排 JMS 自動配置

問題描述

有些用戶會匯入一些 Spring Cloud Azure Starter,以自動設定服務總線 JMS 以外的 Azure 服務。 它們也會使用 Spring JMS 架構,而不需要服務總線 JMS。 然後,當應用程式嘗試啟動時,會擲回下列例外狀況:

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

原因分析

之所以發生此問題,是因為所有 Spring Cloud Azure 自動設定類別都放在相同的模組中,因此任何 Spring Cloud Azure Starter 實際上都會匯入所有自動設定,這也包含服務總線 JMS。 然後,當應用程式使用 Spring JMS API 時,它會符合 服務總線 JMS 自動設定 並觸發它的條件。 然後,對於不想使用 spring-cloud-azure-starter-servicebus-jms的使用者而言,屬性條件是不符合的,因為沒有理由為 JMS 設定服務總線。 這種情況會導致引發例外狀況。

解決方案

Spring Cloud Azure for 服務匯流排 JMS 提供屬性來開啟或關閉其自動設定。 您可以使用下列屬性設定,視需要停用此功能:

spring.jms.servicebus.enabled=false

設定訊息屬性

如何設定輸出訊息的內容類型?

若要設定內容類型,請自定義訊息轉換器,以在轉換訊息時修改內容類型屬性。 下列程式代碼會以位元組訊息作為範例。

首先,自定義要在 JmsTemplate中使用的訊息轉換器,如下列範例所示:

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;
    }
}

然後,宣告您的自定義訊息轉換器 Bean,如下列範例所示:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

如何設定 MappingJackson2MessageConverter 的類型標識碼屬性名稱?

type-id-property-name 屬性可讓 MappingJackson2MessageConverter 判斷要用來還原串行化訊息承載的類別。 將每個 Java 物件串行化為 Spring Message 承載時,轉換器會將承載類型儲存到訊息屬性中,並將屬性名稱記錄為 type-id-property-name。 然後,還原串行化訊息時,轉換器會從訊息讀取類型標識碼,並執行還原串行化。

若要設定 type-id-property-name,請宣告您自己的 MappingJackson2MessageConverter Bean,並設定該屬性,如下列範例所示:

@Configuration(proxyBeanMethods = false)
public class CustomJmsConfiguration {

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

重複偵測

Azure 服務總線支援 重複偵測,它會套用 MessageId 屬性來唯一識別訊息,並捨棄傳送至服務總線的重複專案。

不過,針對 JMS API,您不應該設定 JMS 訊息識別碼,這被視為 JMS 規格中的 非法。 因此,Spring Cloud Azure 服務總線 JMS 入門版目前不支援此功能。

如需此功能的任何進一步更新,請參閱 GitHub 問題。

啟用AMQP傳輸記錄

如需詳細資訊,請參閱 服務匯流排 問題疑難排解 中的 啟用 AMQP 傳輸記錄 一節。

取得更多協助

如需連絡支援方式的詳細資訊,請參閱存放庫根目錄 支援。

Spring Cloud Azure 服務總線 JMS 入門版的資源

提出 GitHub 問題

提出 GitHub 問題時,會要求下列詳細數據:

  • 服務總線組態/ 命名空間環境
    • 哪一層是命名空間 (標準或進階)?
    • 使用的是哪種類型的訊息實體(佇列或主題)? 及其組態。
    • 每個訊息的平均大小為何?
  • 流量模式是什麼樣子? (也就是說,每分鐘的訊息數量,以及用戶端是否一直處於忙碌狀態,或者是否有流量較低的時段。)
  • 重現程式代碼和步驟
    • 這點很重要,因為我們通常無法在我們的環境中重現該問題。
  • 記錄檔