SpringBoot與ActiveMQ整合實現手動ACK

        看這篇文章之前,我相信大家已經有過ActiveMQ的基本知識,已經知道JmsTemplete、MessageListenerContainer、Connection、Session、ConnectionFactory等相關知識,在後續的介紹裏,還是會簡單的進行介紹,以便更好的瞭解。

      項目搭建

         和SpringBoot整合之前,我們先導入相關依賴包,如下所示:

<!--activemq依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!--測試依賴包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

           application.properties相關配置:

#配置ActiveMq
spring.activemq.broker-url=tcp://127.0.0.1:61616
spring.activemq.password=admin
spring.activemq.user=admin
#隊列名稱
activemq.first.queue=FIRST_QUEUE

        下面我們先實現生產者和消費者:

        JmsTemplate是消息處理核心類(線程安全),被用作消息的發送和接受,在發送或者是消費消息的同時也會對所需資源進行創建和釋放。消息消費採用receive方式(同步、阻塞的),這裏不做具體描述。關於消息的發送,常用的方式爲send()以及convertAndSend()兩種,其中send()發送需要我們自己指定消息格式,使用convertAndSend可以根據定義好的MessageConvert自動轉換消息格式。大家請注意,在自定義JmsTemplate時,一定要指定ConnectionFactory,否則會出錯。

          生產者,使用JmsTemplate.convertAndSend()實現消息的發送。

@Component
@Slf4j
public class FirstQueueProducer {
    @Resource
    private JmsTemplate jmsTemplate;

    /**
     * 用於發送消息到mq服務
     * @param destination queue或者topic
     * @param message 消息體
     */
    public void send(String destination, String message) {
        this.jmsTemplate.convertAndSend(destination,message);
        log.info("發送消息成功,發送方式:{},發送內容:{}",destination,message);
    }
}

        消費者,這裏使用@JmsListener監聽隊列消息,大家請注意,如果我們不在@JmsListener中指定containerFactory,那麼將使用默認配置,默認配置中Session是開啓了事物的,即使我們設置了手動Ack也是無效的。在後續配置containerFactory的時候會給大家提到這個地方的坑。

@Component
@Slf4j
public class FirstQueueConsumer {
    @JmsListener(destination = "${activemq.first.queue}")
    public void consumer(ActiveMQMessage message) throws JMSException {
        log.info("接受隊列消息,內容爲:{}",message.getStringProperty("value"));
    }
}

        到目前爲止,一個簡單的生產和消費就實現了,大家可以測試一下,試試看。在測試的時候,如果不生效,請在啓動類上添加@EnableJms。大家測試,或許應該發現了,當使用默認配置的時候,在不出現異常的情況下,消息會被成功消費,如果消費過程中出現異常,即使已經獲取到消息也會消費失敗,消息還是保留在消息隊列中。這就是上述給大家說的,採用默認配置,Session是自動開啓了事物的,如果消費成功(不出現任何異常的情況下)會提交事物,否則會回滾事物。

     手動ACK

       默認的配置已經能夠滿足大部分的業務,但是在某些情況下,我們可能需要手動確認消息的消費,這樣我們就不得不修改默認的配置。在修改默認配置之前,我們先來了解一下ACK_MOD的幾種類型:

int AUTO_ACKNOWLEDGE = 1;自動確認
int CLIENT_ACKNOWLEDGE = 2;客戶端手動確認
int DUPS_OK_ACKNOWLEDGE = 3;批量自動確認
int SESSION_TRANSACTED = 0;事物提交確認

       這幾種是javax.jms.Session提供給客戶端使用,如果我們想手動確認消息,那麼肯定是需要CLIENT_ACKNOWLEDGE = 2這個值了,真的嗎?其實即使我們把commit模式修改爲2並且關閉事物,也不會起到任何作用,大家不妨試一試,源碼如下:

   //org.springframework.jms.listener.AbstractMessageListenerContainer

protected void commitIfNecessary(Session session, Message message) throws JMSException {
    if (session.getTransacted()) {//是否開啓事物
        if (this.isSessionLocallyTransacted(session)) {
            JmsUtils.commitIfNecessary(session);//進行事物提交
        }
        //判斷消息不爲空並且是否設置爲CLIENT_ACKNOWLEDGE
    } else if (message != null && this.isClientAcknowledge(session)) {
        message.acknowledge();
    }

}

  //org.springframework.jms.support.JmsAccessor

protected boolean isClientAcknowledge(Session session) throws JMSException {
   return (session.getAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE);
}

        從源碼中可以發現,即使我們設置ACK_MOD爲CLIENT_ACKNOWLEDGE,也是不起作用的,Spring在判斷的時候,直接轉化爲自動提交了。難道就不能實現手動提交了嗎?當然不是,ActiveMQ在jms的基礎上新添加了一個模式:INDIVIDUAL_ACKNOWLEDGE = 4(單條消息確認)。使用該模式就能夠滿足我們的需要,下面簡單介紹幾種使用該模式的方式:

     自定義JmsTemplate

       最簡單,最直接的方式就是自定義JmsTemplate,如下:

@Bean
public JmsTemplate jmsTemplate(ActiveMQConnectionFactory factory) {
    JmsTemplate jmsTemplate = new JmsTemplate();
    //關閉事物
    jmsTemplate.setSessionTransacted(false);
    //設置爲單條消息確認
    jmsTemplate.setSessionAcknowledgeMode(4);
    jmsTemplate.setConnectionFactory(factory);
    return jmsTemplate;
}

         在消費消息時,直接使用該JmsTemplate.receive操作消息即可。較少使用。

      使用DefaultMessageListenerContainer

         DefaultMessageListenerContainer繼承AbstractPollingMessageListenerContainer,採用consumer.receive來接受消息,支持XA transaction,receive方式是同步的,阻塞的,依賴多線程(taskExecutor)處理消息。

         採用該方式手動Ack代碼如下:

@Bean
public DefaultMessageListenerContainer defaultMessageListenerContainer(ConnectionFactory  connectionFactory){
    DefaultMessageListenerContainer defaultMessageListenerContainer=new DefaultMessageListenerContainer();
    defaultMessageListenerContainer.setSessionAcknowledgeMode(4);//設置單條消息確認
    defaultMessageListenerContainer.setDestinationName("FIRST_QUEUE");//需要指定隊列
    defaultMessageListenerContainer.setSessionTransacted(false);//關閉事物,否則不生效
    //消費消息
    defaultMessageListenerContainer.setMessageListener((MessageListener) message -> {
        try {
            System.out.println("消費消息:"+message);
            //手動確認消息,如若不確認,消息將一直存在於消息隊列中
            message.acknowledge();
        } catch (JMSException e) {
            e.printStackTrace();
        }
    });
    defaultMessageListenerContainer.setConnectionFactory(connectionFactory);
    return defaultMessageListenerContainer;
}

       在驗證該方式之前,需要把之前寫的FirstQueueConsumer消費者註釋掉,以免被它消費自動確認,影響結果。使用該方式需要明確指定destination和messageListener。也就是說有多少隊列或主題就需要定義多少個這樣的Bean。

     使用SimpleJmsListenerContainerFactory

       SimpleJmsListenerContainerFactory內部使用SimpleMessageListenerContainer,該容器使用簡單,不支持外部事物,另外該方式支持持有多個MessageConsumer實例,並且它提供了兩種方式處理消息:線程池(taskExecutor)和MessageConsumer.setMessageListener。源碼如下:

protected MessageConsumer createListenerConsumer(final Session session) throws JMSException {
   Destination destination = getDestination();
   if (destination == null) {
      destination = resolveDestinationName(session, getDestinationName());
   }
   MessageConsumer consumer = createConsumer(session, destination);
    //當線程池不爲空
   if (this.taskExecutor != null) {
      consumer.setMessageListener(new MessageListener() {
         @Override
         public void onMessage(final Message message) {
            //使用線程池消費消息
            taskExecutor.execute(new Runnable() {
               @Override
               public void run() {
                  processMessage(message, session);
               }
            });
         }
      });
   }
   else {
       //使用MessageConsumer.setMessageListener()處理消息
      consumer.setMessageListener(new MessageListener() {
         @Override
         public void onMessage(Message message) {
            processMessage(message, session);
         }
      });
   }

   return consumer;
}

       在我們瞭解該容器之後,再來看看該容器如何實現手動Ack,代碼如下:

@Bean
    public SimpleJmsListenerContainerFactory firstFactory(ConnectionFactory  connectionFactory) {
       SimpleJmsListenerContainerFactory factory=new SimpleJmsListenerContainerFactory();
       factory.setSessionTransacted(false);
       factory.setSessionAcknowledgeMode(4);
       factory.setConnectionFactory(connectionFactory);
       return factory;
    }

       定義好容器之後,我們就需要在消費者上去使用它,如下:

@JmsListener(destination = "${activemq.first.queue}" ,containerFactory = "firstFactory")

       大家在測試的時候,也可以在消費者方法裏輸出Session對象中的事物開啓狀態和Ack模式,以便驗證是否成功,如下:

@JmsListener(destination = "${activemq.first.queue}" ,containerFactory = "firstFactory")
public void consumer(ActiveMQMessage message,Session session) throws Exception {
    System.out.println(session.getAcknowledgeMode());
    System.out.println(session.getTransacted());
    log.info("接受隊列消息,內容爲:{}",message);
    message.acknowledge();
}

         @JmsListener中containerFactory可以指定容器工廠,Spring提供的容器工廠有兩種: DefaultJmsListenerContainerFactory和SimpleJmsListenerContainerFactory。網上很多示例使用的都是DefaultJmsListenerContainerFactory,例如:

@Bean
public DefaultJmsListenerContainerFactory firstFactory(ConnectionFactory connectionFactory, DefaultJmsListenerContainerFactoryConfigurer configurer) {
    DefaultJmsListenerContainerFactory defaultJmsListenerContainerFactory = new DefaultJmsListenerContainerFactory();
    defaultJmsListenerContainerFactory.setSessionTransacted(false);
    defaultJmsListenerContainerFactory.setSessionAcknowledgeMode(4);
    configurer.configure(defaultJmsListenerContainerFactory, connectionFactory);
    return defaultJmsListenerContainerFactory;
}

       但是此方法在我這裏並沒有生效,不知是否是我這裏使用有誤,如果大家可以實現,望不吝賜教。

    總結

      上述內容是我在SpringBoot與ActiveMQ整合實現消息手動確認時,查閱網上資料,根據個人感悟所得,其中若有不好或是不正確的地方,望大家不吝指教,共同進步。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章