Spring整合rabbitmq實踐(二):擴展
Spring整合rabbitmq實踐(三):源碼
1. Rabbitmq基本概念
1.1. Rabbitmq中的各個角色
producer:消息生產者;
consumer:消息消費者;
queue:消息隊列;
exchange:接收producer發送的消息按照binding規則轉發給相應的queue;
binding:exchange與queue之間的關系;
virtualHost:每個virtualHost持有自己的exchange、queue、binding,用戶只能在virtualHost粒度控制權限。
1.2. exchange的幾種類型
fanout:
群發到所有綁定的queue;
direct:
根據routing key routing到相應的queue,routing不到任何queue的消息扔掉;可以不同的key綁到同一個queue,也可以同一個key綁到不同的queue;
topic:
類似direct,區別是routing key是由一組以“.”分隔的單詞組成,可以有通配符,“*”匹配一個單詞,“#”匹配0個或多個單詞;
headers:
根據arguments來routing。
arguments為一組key-value對,任意設置。
“x-match”是一個特殊的key,值為“all”時必須匹配所有argument,值為“any”時只需匹配任意一個argument,不設置默認為“all”。
2. spring整合rabbitmq
通過以下配置,可以獲得最基礎的發送消息到queue,以及從queue接收消息的功能。
2.1. maven依賴
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>${version}</version>
</dependency>
這個包同時包含了一些其它的包:spring-context、spring-tx、spring-web、spring-messaging、spring-retry、spring-amqp、amqp-client,如果想單純一點,可以單獨引入。
最主要的是以下幾個包,
spring-amqp:
Spring AMQP Core.
spring-rabbit:
Spring RabbitMQ Support.
amqp-client:
The RabbitMQ Java client library allows Java applications to interface with RabbitMQ.
個人理解就是,spring-amqp是spring整合的amqp,spring-rabbit是spring整合的rabbitmq(rabbitmq是amqp的一個實現,所以可能spring-rabbit也是類似關系),amqp-client提供操作rabbitmq的java api。
目前最新的是2.0.5.RELEASE版本。如果編譯報錯,以下信息或許能有所幫助:
(1)
java.lang.ClassNotFoundException: org.springframework.amqp.support.converter.SmartMessageConverter
解決方案:spring-amqp版本改為2.0.5.RELEASE。
(2)
java.lang.IllegalStateException: LifecycleProcessor not initialized - call 'refresh' before invoking lifecycle methods via the context...
解決方案:spring-context版本改為5.0.7.RELEASE。
(3)
java.lang.NoSuchMethodError: org.springframework.util.ObjectUtils.unwrapOptional(Ljava/lang/Object;)Ljava/lang/Object
解決方案:spring-core版本改為5.0.7.RELEASE。
(4)
java.lang.NullPointerException: null
at org.springframework.core.BridgeMethodResolver.findBridgedMethod(BridgeMethodResolver.java:60)
at org.springframework.beans.GenericTypeAwarePropertyDescriptor.<init>(GenericTypeAwarePropertyDescriptor.java:70)
at org.springframework.beans.CachedIntrospectionResults.buildGenericTypeAwarePropertyDescriptor(CachedIntrospectionResults.java:366)
at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:302)
解決方案:spring-beans版本改為5.0.7.RELEASE。
(5)
Caused by: java.lang.NoSuchMethodError: org.springframework.aop.framework.AopProxyUtils.getSingletonTarget(Ljava/lang/Object;)Ljava/lang/Object;
解決方案:spring-aop版本改為5.0.7.RELEASE。
總之,需要5.0.7.RELEASE版本的spring,及相匹配版本的amqp-client。
2.2. 配置ConnectionFactory
后面所講的這些bean配置,spring-amqp中都有默認配置,如果不需要修改默認配置,則不用人為配置這些bean。后面這些配置也沒有涉及到所有的屬性。
這里的ConnectionFactory指的是spring-rabbit包下面的ConnectionFactory接口,不是amqp-client包下面的ConnectionFactory類。
@Configuration
public class MqProducerConfig {
@Autowired
@Bean
public ConnectionFactory amqpConnectionFactory(ConnectionListener connectionListener,
RecoveryListener recoveryListener,
ChannelListener channelListener) {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses("localhost:5672");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
connectionFactory.setChannelCacheSize(25);
connectionFactory.setChannelCheckoutTimeout(0);
connectionFactory.setPublisherReturns(false);
connectionFactory.setPublisherConfirms(false);
connectionFactory.addConnectionListener(connectionListener);
connectionFactory.addChannelListener(channelListener);
connectionFactory.setRecoveryListener(recoveryListener);
//connectionFactory.setConnectionCacheSize(1);
//connectionFactory.setConnectionLimit(Integer.MAX_VALUE);
return connectionFactory;
}
}
上面這個bean是spring-amqp的核心,不論是發送消息還是接收消息都需要這個bean,下面描述一下里面這些配置的含義。
setAddresses:設置了rabbitmq的地址、端口,集群部署的情況下可填寫多個,“,”分隔。
setUsername:設置rabbitmq的用戶名。
setPassword:設置rabbitmq的用戶密碼。
setVirtualHost:設置virtualHost。
setCacheMode:設置緩存模式,共有兩種,CHANNEL和CONNECTION模式。
CHANNEL模式,程序運行期間ConnectionFactory會維護著一個Connection,所有的操作都會使用這個Connection,但一個Connection中可以有多個Channel,操作rabbitmq之前都必須先獲取到一個Channel,否則就會阻塞(可以通過setChannelCheckoutTimeout()設置等待時間),這些Channel會被緩存(緩存的數量可以通過setChannelCacheSize()設置);
CONNECTION模式,這個模式下允許創建多個Connection,會緩存一定數量的Connection,每個Connection中同樣會緩存一些Channel,除了可以有多個Connection,其它都跟CHANNEL模式一樣。
這里的Connection和Channel是spring-amqp中的概念,并非rabbitmq中的概念,官方文檔對Connection和Channel有這樣的描述:
Sharing of the connection is possible since the "unit of work" for messaging with AMQP is actually a "channel" (in some ways, this is similar to the relationship between a Connection and a Session in JMS).
關于CONNECTION模式中,可以存在多個Connection的使用場景,官方文檔的描述:
The use of separate connections might be useful in some environments, such as consuming from an HA cluster, in conjunction with a load balancer, to connect to different cluster members.
setChannelCacheSize:設置每個Connection中(注意是每個Connection)可以緩存的Channel數量,注意只是緩存的Channel數量,不是Channel的數量上限,操作rabbitmq之前(send/receive message等)要先獲取到一個Channel,獲取Channel時會先從緩存中找閑置的Channel,如果沒有則創建新的Channel,當Channel數量大于緩存數量時,多出來沒法放進緩存的會被關閉。
注意,改變這個值不會影響已經存在的Connection,只影響之后創建的Connection。
setChannelCheckoutTimeout:當這個值大于0時,channelCacheSize不僅是緩存數量,同時也會變成數量上限,從緩存獲取不到可用的Channel時,不會創建新的Channel,會等待這個值設置的毫秒數,到時間仍然獲取不到可用的Channel會拋出AmqpTimeoutException異常。
同時,在CONNECTION模式,這個值也會影響獲取Connection的等待時間,超時獲取不到Connection也會拋出AmqpTimeoutException異常。
setPublisherReturns、setPublisherConfirms:producer端的消息確認機制(confirm和return),設為true后開啟相應的機制,后文詳述。
官方文檔描述publisherReturns設為true打開return機制,publisherComfirms設為true打開confirm機制,但測試結果(2.0.5.RELEASE版本)是,任意一個設為true,兩個都會打開。
addConnectionListener、addChannelListener、setRecoveryListener:添加或設置相應的Listener,后文詳述。
setConnectionCacheSize:僅在CONNECTION模式使用,設置Connection的緩存數量。
setConnectionLimit:僅在CONNECTION模式使用,設置Connection的數量上限。
上面的bean配置,除了需要注入的幾個listener bean以外,其它設置的都是其默認值(2.0.5.RELEASE版本),后面的bean示例配置也是一樣,部分屬性不同版本的默認值可能有所不同。
2.3. 配置com.rabbitmq.client.ConnectionFactory
一般不用配置這個bean,這里簡單提一下。
這個ConnectionFactory是rabbit api中的ConnectionFactory類,這里面是連接rabbitmq節點的Connection配置。
/** Default user name */
public static final String DEFAULT_USER = "guest";
/** Default password */
public static final String DEFAULT_PASS = "guest";
/** Default virtual host */
public static final String DEFAULT_VHOST = "/";
/** Default maximum channel number;
* zero for unlimited */
public static final int DEFAULT_CHANNEL_MAX = 0;
/** Default maximum frame size;
* zero means no limit */
public static final int DEFAULT_FRAME_MAX = 0;
/** Default heart-beat interval;
* 60 seconds */
public static final int DEFAULT_HEARTBEAT = 60;
/** The default host */
public static final String DEFAULT_HOST = "localhost";
/** 'Use the default port' port */
public static final int USE_DEFAULT_PORT = -1;
/** The default non-ssl port */
public static final int DEFAULT_AMQP_PORT = AMQP.PROTOCOL.PORT;
/** The default ssl port */
public static final int DEFAULT_AMQP_OVER_SSL_PORT = 5671;
/** The default TCP connection timeout: 60 seconds */
public static final int DEFAULT_CONNECTION_TIMEOUT = 60000;
/**
* The default AMQP 0-9-1 connection handshake timeout. See DEFAULT_CONNECTION_TIMEOUT
* for TCP (socket) connection timeout.
*/
public static final int DEFAULT_HANDSHAKE_TIMEOUT = 10000;
/** The default shutdown timeout;
* zero means wait indefinitely */
public static final int DEFAULT_SHUTDOWN_TIMEOUT = 10000;
/** The default continuation timeout for RPC calls in channels: 10 minutes */
public static final int DEFAULT_CHANNEL_RPC_TIMEOUT = (int) MINUTES.toMillis(10);
/** The default network recovery interval: 5000 millis */
public static final long DEFAULT_NETWORK_RECOVERY_INTERVAL = 5000;
private static final String PREFERRED_TLS_PROTOCOL = "TLSv1.2";
private static final String FALLBACK_TLS_PROTOCOL = "TLSv1";
...
如果想修改這些配置,可以按如下方式配置:
@Autowired
@Bean
public ConnectionFactory connectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);
return connectionFactory;
}
@Bean
public com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory() {
com.rabbitmq.client.ConnectionFactory connectionFactory = new com.rabbitmq.client.ConnectionFactory();
connectionFactory.setAutomaticRecoveryEnabled(false);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
return connectionFactory;
}
2.4. 配置AmqpTemplate
consumer端如果通過@RabbitListener注解的方式接收消息,不需要這個bean。
不建議直接通過ConnectionFactory獲取Channel操作rabbitmq,建議通過amqpTemplate操作。
@Autowired
@Bean
public AmqpTemplate amqpTemplate(ConnectionFactory amqpConnectionFactory,
RabbitTemplate.ReturnCallback returnCallback,
RabbitTemplate.ConfirmCallback confirmCallback,
RetryTemplate retryTemplate,
MessageConverter messageConverter
){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(amqpConnectionFactory);
rabbitTemplate.setRetryTemplate(retryTemplate);
rabbitTemplate.setMessageConverter(messageConverter);
rabbitTemplate.setChannelTransacted(false);
rabbitTemplate.setReturnCallback(returnCallback);
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setMandatory(false);
return rabbitTemplate;
}
setConnectionFactory:設置spring-amqp的ConnectionFactory。
setRetryTemplate:設置重試機制,詳情見后文。
setMessageConverter:設置MessageConverter,用于java對象與Message對象(實際發送和接收的消息對象)之間的相互轉換,詳情見后文。
setChannelTransacted:打開或關閉Channel的事務,關于amqp的事務后文描述。
setReturnCallback、setConfirmCallback:return和confirm機制的回調接口,后文詳述。
setMandatory:設為true使ReturnCallback生效。
2.5. 配置RabbitListenerContainerFactory
這個bean僅在consumer端通過@RabbitListener注解的方式接收消息時使用,每一個@RabbitListener注解的方法都會由這個RabbitListenerContainerFactory創建一個MessageListenerContainer,負責接收消息。
@Autowired
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory cachingConnectionFactory,
ErrorHandler errorHandler,
MessageConverter messageConverter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(cachingConnectionFactory);
factory.setMessageConverter(messageConverter);
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(250);
factory.setChannelTransacted(false);
factory.setTxSize(1);
factory.setDefaultRequeueRejected(true);
factory.setErrorHandler(errorHandler);
return factory;
}
setConnectionFactory:設置spring-amqp的ConnectionFactory。
setMessageConverter:對于consumer端,MessageConverter也可以在這里配置。
setAcknowledgeMode:設置consumer端的應答模式,共有三種:NONE、AUTO、MANUAL。
NONE,無應答,這種模式下rabbitmq默認consumer能正確處理所有發出的消息,所以不管消息有沒有被consumer收到,有沒有正確處理都不會恢復;
AUTO,由Container自動應答,正確處理發出ack信息,處理失敗發出nack信息,rabbitmq發出消息后將會等待consumer端的應答,只有收到ack確認信息才會把消息清除掉,收到nack信息的處理辦法由setDefaultRequeueRejected()方法設置,所以在這種模式下,發生錯誤的消息是可以恢復的。
MANUAL,基本同AUTO模式,區別是需要人為調用方法給應答。
setConcurrentConsumers:設置每個MessageListenerContainer將會創建的Consumer的最小數量,默認是1個。
setMaxConcurrentConsumers:設置每個MessageListenerContainer將會創建的Consumer的最大數量,默認等于最小數量。
setPrefetchCount:設置每次請求發送給每個Consumer的消息數量。
setChannelTransacted:設置Channel的事務。
setTxSize:設置事務當中可以處理的消息數量。
setDefaultRequeueRejected:設置當rabbitmq收到nack/reject確認信息時的處理方式,設為true,扔回queue頭部,設為false,丟棄。
setErrorHandler:實現ErrorHandler接口設置進去,所有未catch的異常都會由ErrorHandler處理。
2.6. sending messages
AmqpTamplate里面有下面幾個方法可以向queue發送消息:
void send(Message message) throws AmqpException;
void send(String routingKey, Message message) throws AmqpException;
void send(String exchange, String routingKey, Message message) throws AmqpException;
這里,exchange必須存在,否則消息發不出去,會看到錯誤日志,但不影響程序運行:
03:57:38.700 [AMQP Connection 127.0.0.1:5672] ERROR [CachingConnectionFactory.java:956] - Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'xxx' in vhost 'test', class-id=60, method-id=40)
Message是org.springframework.amqp.core.Message類,spring-amqp發送和接收的都是這個Message。
package org.springframework.amqp.core;
public class Message implements Serializable {
private static final long serialVersionUID = -7177590352110605597L;
private static final String ENCODING = Charset.defaultCharset().name();
private final MessageProperties messageProperties;
private final byte[] body;
public Message(byte[] body, MessageProperties messageProperties) { //NOSONAR
this.body = body; //NOSONAR
this.messageProperties = messageProperties;
}
...
}
從Message類源碼可以看到消息內容放在byte[]里面,MessageProperties對象包含了非常多的一些其它信息,如Header、exchange、routing key等。
這種方式,需要將消息內容(String,或其它Object)轉換為byte[],示例:
amqpTemplate.send(exchange, routingKey, new Message(JSON.toJSONString(event).getBytes(), MessagePropertiesBuilder.newInstance().build()));
也可以直接調用下面幾個方法,Object將會自動轉為Message對象發送:
void convertAndSend(Object message) throws AmqpException;
void convertAndSend(String routingKey, Object message) throws AmqpException;
void convertAndSend(String exchange, String routingKey, Object message)
throws AmqpException;
2.7. receiving messages
有兩種方法接收消息:
1.polling consumer,輪詢調用方法一次獲取一條;
2.asynchronous consumer,listener異步接收消息。
polling consumer
直接通過AmqpTemplate的方法從queue獲取消息,有如下方法:
Message receive() throws AmqpException;
Message receive(String queueName) throws AmqpException;
Message receive(long timeoutMillis) throws AmqpException;
Message receive(String queueName, long timeoutMillis) throws AmqpException;
如果queue里面沒有消息,會立刻返回null;傳入timeoutMillis參數后可阻塞等待一段時間。
如果想直接從queue獲取想要的java對象,可調用下面這一組方法:
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
Object receiveAndConvert(long timeoutMillis) throws AmqpException;
Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException;
<T> T receiveAndConvert(ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(String queueName, ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference<T> type) throws AmqpException;
<T> T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference<T> type)
throws AmqpException;
后面4個方法是帶泛型的,示例如下:
Foo<Bar<Baz, Qux>> foo =
rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference<Foo<Bar<Baz, Qux>>>() { });
使用這四個方法需要配置org.springframework.amqp.support.converter.SmartMessageConverter,這是一個接口,Jackson2JsonMessageConverter已經實現了這個接口,所以只要將Jackson2JsonMessageConverter設置到RabbitTemplate中即可。
asynchronous consumer
有多種方式可以實現,詳情參考官方文檔。
最簡單的實現方式是@RabbitListener注解,示例:
@Component
public class RabbitMqListener {
@RabbitListener(queues = "queueName")
public void listen(Message message) {
JSON.parseObject(new String(message.getBody()), typeReference);
}
}
這里接收消息的對象用的是Message,也可以是自定義的java對象,但調用Converter轉換失敗會報錯。
注解上指定的queue必須是已經存在并且綁定到某個exchange的,否則會報錯:
03:46:59.746 [SimpleAsyncTaskExecutor-1] WARN o.s.a.r.l.BlockingQueueConsumer [BlockingQueueConsumer.java:565] - Failed to declare queue:xxx
03:46:59.747 [SimpleAsyncTaskExecutor-1] WARN o.s.a.r.l.BlockingQueueConsumer [BlockingQueueConsumer.java:479] - Queue declaration failed; retries left=3
org.springframework.amqp.rabbit.listener.BlockingQueueConsumer$DeclarationException: Failed to declare queue(s):[xxx]
如果在@RabbitListener注解中指明binding信息,就能自動創建queue、exchange并建立binding關系。
direct和topic類型的exchange需要routingKey,示例:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "myQueue", durable = "true"),
exchange = @Exchange(value = "auto.exch", durable = "true"),
key = "orderRoutingKey.#")
)
fanout類型的exchange,示例:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "myQueue", durable = "true"),
exchange = @Exchange(value = "auto.exch", durable = "true", type = ExchangeTypes.FANOUT)
)
)
2.0版本之后,可以指定多個routingKey,示例:
key = { "red", "yellow" }
并且支持arguments屬性,可用于headers類型的exchange,示例:
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "auto.headers", autoDelete = "true",
arguments = @Argument(name = "x-message-ttl", value = "10000",
type = "java.lang.Integer")),
exchange = @Exchange(value = "auto.headers", type = ExchangeTypes.HEADERS, autoDelete = "true"),
arguments = {
@Argument(name = "x-match", value = "all"),
@Argument(name = "foo", value = "bar"),
@Argument(name = "baz")
})
)
@Queue有兩個參數exclusive和autoDelete順便解釋一下:
exclusive,排他隊列,只對創建這個queue的Connection可見,Connection關閉queue刪除;
autoDelete,沒有consumer對這個queue消費時刪除。
對于這兩種隊列,durable=true是不起作用的。
另外,如果注解申明的queue和exchange及binding關系都已經存在,但與已存在的設置不同,比如,已存在的exchange的是direct類型,這里嘗試改為fanout類型,結果是不會有任何影響,不論是修改或者新增參數都不會生效。
如果queue存在,exchange存在,但沒有binding,那么程序啟動后會自動建立起binding關系。