springboot下使用rabbitMQ之開發配置方式(一)
距離上次發佈博客已經小一年了,這次...嗯,沒錯,我又回來啦...😂😂😂
本次結合着B站某MQ視頻以及最近在MQ上的實踐聊一聊個人在使用rabbitMQ中所得。
在本章開始前,默認您已通過各種途徑安裝並在springboot中集成了rabbitMQ~
一.是否需要在配置類中定義exchange、queue、routingKey及綁定關係
這個問題我先不表結論,先看一下如果沒有定義會出現什麼問題吧?
在沒有新增default.yy
這個queue的情況下新建個consumer(別忘記@Service註解),具體代碼如下:
@RabbitListener(queues = "default.yy")
public void execYY(Message message, Channel channel){
LOG.info("default.yy 被執行...");
}
啓動springboot後立馬會看到以下錯誤
2023-07-17 10:11:10.534 -> [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#1-1] -> WARN o.s.amqp.rabbit.listener.BlockingQueueConsumer:753 - Failed to declare queue: default.yy
2023-07-17 10:11:10.540 -> [org.springframework.amqp.rabbit.RabbitListenerEndpointContainer#1-1] -> WARN o.s.amqp.rabbit.listener.BlockingQueueConsumer:687 - Queue declaration failed; retries left=3
org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[default.yy]
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:760)
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.passiveDeclarations(BlockingQueueConsumer.java:637)
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.start(BlockingQueueConsumer.java:624)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.initialize(SimpleMessageListenerContainer.java:1385)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1230)
at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.io.IOException: null
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:129)
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:125)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:147)
at com.rabbitmq.client.impl.ChannelN.queueDeclarePassive(ChannelN.java:1012)
at com.rabbitmq.client.impl.ChannelN.queueDeclarePassive(ChannelN.java:46)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory$CachedChannelInvocationHandler.invoke(CachingConnectionFactory.java:1162)
at jdk.proxy2/jdk.proxy2.$Proxy109.queueDeclarePassive(Unknown Source)
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.attemptPassiveDeclarations(BlockingQueueConsumer.java:738)
... 5 common frames omitted
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'default.yy' in vhost 'vhost', class-id=50, method-id=10)
at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:502)
at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:293)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:141)
... 14 common frames omitted
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'default.yy' in vhost 'vhost', class-id=50, method-id=10)
at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:517)
at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:341)
at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:182)
at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:114)
at com.rabbitmq.client.impl.AMQConnection.readFrame(AMQConnection.java:739)
at com.rabbitmq.client.impl.AMQConnection.access$300(AMQConnection.java:47)
at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:666)
... 1 common frames omitted
這是重點Failed to declare queue(s):[default.yy]
看來確實有必要配置,不然找不到絕對會拋錯😂
接下來看看在springboot開發中可以配置方法
二. 可以配置的方法
方法一. 使用配置類(@Configuration+@Bean)的方式配置
- 具體代碼如下
import com.mee.api.common.enums.RabbitMQCfgEnum;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
//使用注入方式聲明對應的Queue
@Bean("yyQueue")
public Queue defaultQueue() {
// durable:是否持久化,默認是false,持久化隊列:會被存儲在磁盤上,當消息代理重啓時仍然存在,暫存隊列:當前連接有效
// exclusive:默認也是false,只能被當前創建的連接使用,而且當連接關閉後隊列即被刪除。此參考優先級高於durable
// autoDelete:是否自動刪除,當沒有生產者或者消費者使用此隊列,該隊列會自動刪除。
return new Queue("yy.queue", true,false,false);
}
//聲明交換機,不同的交換機類型不同:DirectExchange/FanoutExchange/TopicExchange/HeadersExchange
@Bean("defaultExchange")
public c defaultExchange() {
return new DirectExchange("yy_exchange", true, false);
}
//綁定關係:將隊列和交換機綁定, 並設置用於匹配鍵:routingKey
@Bean("queueBinding")
public Binding queueBinding(@Qualifier("yyQueue") Queue defaultQueue,@Qualifier("yyExchange") DirectExchange defaultExchange) {
return BindingBuilder.bind(defaultQueue).to(defaultExchange).with("");
}
}
以上只是很簡單的一個mq的配置例子,看起來非常好,可以添加非常多的默認參數,配置無誤之後啓動即可看到starter已經貼心的爲我們創建好了所需的一切:
這種通用配置方法稍顯麻煩不過也足夠精細,同時你每次啓動時starter都會檢查是否有創建這些配置(在rabbitmq上),沒有就會創建一個,這樣看似好也不好~
再看看有沒有其他方式配置呢?
方法二. 可在業務類中使用註解定義並綁定
- 這是
starter
提供的一套註解方法,使用方式如下:
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.stereotype.Service;
@RabbitListener(
bindings = {
@QueueBinding(
value = @Queue(value = "yy.queue", autoDelete = "false"),
exchange = @Exchange(value = "yy_exchange", type = ExchangeTypes.DIRECT),
key = ""
)
})
@Service
public class MQConsumerYyHandler {
private static final Logger LOG = LoggerFactory.getLogger(MQConsumerYyHandler.class);
@RabbitHandler(isDefault = true)
public void yyDefault(Message message, Channel channel){
// 注意,發送的消息類型必須是實現了Serializable接口的類型,消費者接口類型不能隨便寫!
Object dto = new SimpleMessageConverter().fromMessage(message);
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
LOG.info("接收到消息(deadDefault):{},key:{}", dto,receivedRoutingKey);
}
}
看起來也很不錯,啓動後依然能看到starter爲我們創建(沒有創建的話)exchange、queue以及exchange與queue的綁定(圖略)
這樣看起來似乎比方法一所使用的配置類更清晰結構也更加好。
另需要說明的是@RabbitListener
註解也是可以配置在方法上的,如這樣:
@RabbitListener(
bindings = {
@QueueBinding(
value = @Queue(value = "yy.queue", autoDelete = "false"),
exchange = @Exchange(value = "yy_exchange", type = ExchangeTypes.DIRECT),
key = ""
)
})
@RabbitHandler
public void yyDefault2(Message message, Channel channel){
// 注意,發送的消息類型必須是實現了Serializable接口的類型,消費者接口類型不能隨便寫!
Object dto = new SimpleMessageConverter().fromMessage(message);
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
LOG.info("接收到消息(deadDefault):{},key:{}", dto,receivedRoutingKey);
}
看起來只是位置發生了變化,當然這其中也是有坑的,如果將@RabbitListener
配置在類上則必須指定其上的參數(isDefault):@RabbitHandler(isDefault = true)
,否則springboot無法找到消費者。
那有沒有一種更加靈活的配置方式,當然是有的~
方法三. rabbitMQ後臺手動創建
在rabbitMQ提供的web配置後臺操作大致如下:
這種方法看起來不是很簡便但更加可靠,在一定程度上不會因爲starter自動創建而導致一些莫名其妙的問題,同時使用起來也非常簡單。
更簡潔的使用:
@RabbitListener(queues = "yy.queue")
public void yyDefault(Message message, Channel channel){
// 注意,發送的消息類型必須是實現了Serializable接口的類型,消費者接口類型不能隨便寫!
Object dto = new SimpleMessageConverter().fromMessage(message);
String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey();
LOG.info("接收到消息(deadDefault):{},key:{}", dto,receivedRoutingKey);
}
如果使用的是direct模式+固定單一routingKey則極力推薦方法三的配置方式,如果是多exchange+多queue(或多routingKey) 也只是需要在註解內定義binding參數即可。
最後
基於個人MQ的實踐,總結如下:
- 1.建議先(手動)定義再使用
- 2.配置能簡化應儘量簡化
- 3.一定要弄清楚所使用mq的工作流程再行測試開發(重要)
順帶給下我的配置:
# rabbitMQ
## 配置rabbitmq服務(http://127.0.0.1:15672/#/exchanges)
spring.rabbitmq.host=10.156.122.215
spring.rabbitmq.port=5672
spring.rabbitmq.username=shadow
spring.rabbitmq.password=shadow
spring.rabbitmq.virtual-host=vhost
### 確認消息已發送到交換機(Exchange)
#spring.rabbitmq.publisher-returns=true
### 確認消息已發送到隊列(Queue)
#spring.rabbitmq.publisher-confirm-type=correlated
### 消息發送失敗返回隊列中
#spring.rabbitmq.template.mandatory=true
## 設置手動確認消息
#spring.rabbitmq.listener.simple.acknowledge-mode=manual
#spring.rabbitmq.listener.simple.default-requeue-rejected=false
#spring.rabbitmq.listener.simple.concurrency=1
#spring.rabbitmq.listener.simple.max-concurrency=4
#spring.rabbitmq.listener.simple.prefetch=1
#spring.rabbitmq.listener.simple.retry.enabled=false
### 死信隊列相關配置
#spring.rabbitmq.listener.simple.acknowledge-mode=auto
#spring.rabbitmq.listener.simple.default-requeue-rejected=false
#spring.rabbitmq.listener.simple.retry.enabled=false
#spring.rabbitmq.listener.simple.retry.max-attempts=3
#spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
#spring.rabbitmq.listener.simple.retry.max-interval=3000ms
## 重試
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=3
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms