看這篇文章之前,我相信大家已經有過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整合實現消息手動確認時,查閱網上資料,根據個人感悟所得,其中若有不好或是不正確的地方,望大家不吝指教,共同進步。