3.Reference
3.1 Using Spring AMQP
在本章中,我们将探索接口和类,它们是使用Spring AMQP开发应用程序的基本组件。
3.1.1 AMQP Abstractions(抽象)
Introduction
Spring AMQP由几个模块组成,每个模块在发行版中由一个JAR表示。这些模块是:spring-amqp和spring-rabbit。spring-amqp模块包含org.springframework.amqp.core包。在该包中,您将找到表示核心AMQP“model”的类。我们的目的是提供不依赖于任何特定AMQP代理实现或客户端库的通用抽象。最终用户代码将更易于跨供应商实现移植,因为它只能针对抽象层开发。然后使用这些抽象由特定于broker的模块实现,比如spring-rabbit。目前只有一个RabbitMQ实现;然而,除了RabbitMQ之外,抽象还在. net中使用Apache Qpid进行了验证。由于AMQP原则上在协议级别上运行,因此RabbitMQ客户机可以与任何支持相同协议版本的代理一起使用,但是我们目前不测试任何其他代理。这里的概述假定您已经熟悉AMQP规范的基础知识。如果没有,请查看第5章“其他资源”中列出的资源。
Message
0-9-1 AMQP规范没有定义消息类或接口。相反,在执行basicPublish()等操作时,内容作为字节数组参数传递,其他属性作为单独的参数传递。Spring AMQP将消息类定义为更通用的AMQP域模型表示的一部分。Message类的目的是简单地将主体和属性封装在一个实例中,这样API就可以变得更简单。消息类定义非常简单。
public class Message {
private final MessageProperties messageProperties;
private final byte[] body;
public Message(byte[] body, MessageProperties messageProperties) {
this.body = body;
this.messageProperties = messageProperties;
}
public byte[] getBody() {
return this.body;
}
public MessageProperties getMessageProperties() {
return this.messageProperties;
}
}
MessageProperties接口定义了一些常见的属性,如messageId、时间戳、contentType等。通过调用setHeader(String key, Object value)方法,还可以用用户定义的头扩展这些属性。
重要:从版本1.5.7、1.6.11、1.7.4、2.0.0开始,如果消息体是一个序列化的可序列化java对象,那么在执行toString()操作(例如日志消息)时,它将不再反序列化(默认情况下)。这是为了防止不安全的反序列化。默认情况下,只有java.util和java.lang类是反序列化的。要恢复到前面的行为,可以通过调用Message.addWhiteListPatterns(…)添加允许的类/包模式。支持一个简单的*通配符,例如com.foo.*,*.MyClass。不能反序列化的主体将在日志消息中由byte[<size>]表示。
Exchange
Exchange接口表示AMQP交换,这是消息生成器发送到的对象。在代理的虚拟主机内的每个Exchange都有一个惟一的名称和一些其他属性:
public interface Exchange {
String getName();
String getExchangeType();
boolean isDurable();
boolean isAutoDelete();
Map<String, Object> getArguments();
}
如您所见,Exchange还有一个类型,它由ExchangeTypes中定义的常量表示。基本类型有:Direct、Topic、Fanout和header。在核心包中,您将发现每种类型的Exchange接口的实现。就处理队列绑定的方式而言,这些交换类型之间的行为各不相同。例如,直接交换允许使用固定路由键(通常是队列的名称)绑定队列。主题交换支持具有路由模式的绑定,路由模式可能包含*和#通配符,分别用于1和0或多个通配符。Fanout exchange发布到绑定到它的所有队列,而不考虑任何路由键。有关这些和其他交换类型的更多信息,请参阅第5章“其他资源”。
注意:AMQP规范还要求任何代理提供没有名称的“缺省”直接交换。声明的所有队列都将绑定到该缺省交换器,并将其名称作为路由键。在第3.1.4节“AmqpTemplate”中,您将了解更多关于Spring AMQP中默认Exchange的用法。
Queue
Queue类表示消息使用者从其中接收消息的组件。与各种Exchange类一样,我们的实现也是这个核心AMQP类型的抽象表示。
public class Queue {
private final String name;
private volatile boolean durable;
private volatile boolean exclusive;
private volatile boolean autoDelete;
private volatile Map<String, Object> arguments;
/**
* The queue is durable, non-exclusive and non auto-delete.
*
* @param name the name of the queue.
*/
public Queue(String name) {
this(name, true, false, false);
}
// Getters and Setters omitted for brevity
}
注意,构造函数接受队列名称。根据实现的不同,管理模板可能提供生成惟一命名队列的方法。这种队列可以用作“答复”地址或其他临时情况。因此,自动生成队列的exclusive和autoDelete属性都将设置为true。
注意:
有关使用名称空间支持(包括队列参数)声明队列的信息,请参阅第3.1.10节“配置代理”中关于队列的部分。
Binding
假定生产者向交换器发送消息,消费者从队列接收消息,那么将队列连接到交换器的绑定对于通过消息传递将生产者和消费者连接起来是至关重要的。在Spring AMQP中,我们定义了一个绑定类来表示这些连接。让我们回顾一下将队列绑定到交换器的基本选项。
您可以使用固定路由键将队列绑定到DirectExchange。
new Binding(someQueue, someDirectExchange, "foo.bar")
您可以使用路由模式将队列绑定到TopicExchange。
new Binding(someQueue, someTopicExchange, "foo.*")
您可以将队列绑定到没有路由键的FanoutExchange。
new Binding(someQueue, someFanoutExchange)
我们还提供了一个BindingBuilder来促进“连贯API”风格。
Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*");
注意:为了清晰起见,上面显示了BindingBuilder类,但是当使用bind()方法的静态导入时,这种样式可以很好地工作。
绑定类的实例本身只是保存关于连接的数据。换句话说,它不是一个“活动”组件。但是,正如您稍后将在3.1.10节“配置代理”中看到的,AmqpAdmin类可以使用绑定实例来实际触发代理上的绑定操作。
同样,正如您将在同一节中看到的,绑定实例可以在@Configuration类中使用Spring的@ bean样式定义。还有一个方便的基类,它进一步简化了生成AMQP相关bean定义的方法,并识别队列、交换和绑定,以便在应用程序启动时在AMQP代理上声明它们。
AmqpTemplate也在核心包中定义。作为实际AMQP消息传递中涉及的主要组件之一,在其自己的部分(参见3.1.4节,“AmqpTemplate”)中对其进行了详细讨论。
3.1.2 Connection and Resource Management
Introduction
虽然我们在前一节中描述的AMQP模型是通用的,并且适用于所有实现,但是当我们进入资源管理时,详细信息是特定于代理实现的。因此,在本节中,我们将关注仅存在于“spring-rabbit”模块中的代码,因为此时,RabbitMQ是惟一受支持的实现。
管理到RabbitMQ代理的连接的中心组件是ConnectionFactory接口。ConnectionFactory实现的职责是提供org.springframework.amq.rabbit.connection.Connection 的实例,它是com.rabbitmq.client.Connection的包装器。我们提供的唯一具体实现是CachingConnectionFactory,默认情况下,它建立一个可以由应用程序共享的连接代理。共享连接是可能的,因为使用AMQP进行消息传递的“工作单元”实际上是一个“通道”(在某些方面,这类似于JMS中的连接和会话之间的关系)。可以想象,connection实例提供了一个createChannel方法。CachingConnectionFactory实现支持缓存这些通道,并根据它们是否是事务性的,为通道维护单独的缓存。在创建CachingConnectionFactory实例时,可以通过构造函数提供主机名。还应该提供用户名和密码属性。如果希望配置通道缓存的大小(默认为25),也可以在这里调用setChannelCacheSize()方法。
从1.3版开始,CachingConnectionFactory可以配置为缓存连接和通道。在这种情况下,每次调用createConnection()都会创建一个新的连接(或从缓存中检索空闲连接)。关闭连接将返回到缓存(如果没有达到缓存大小)。在这样的连接上创建的通道也被缓存。在某些环境中,使用单独的连接可能是有用的,比如使用HA集群和负载平衡器连接到不同的集群成员。将cacheMode设置为 CacheMode.CONNECTION。
注意:这并不限制连接的数量,而是指定允许多少空闲打开的连接。
从1.5.5版本开始,提供了一个新的属性connectionLimit。当设置此选项时,它将限制允许的连接总数。设置时,如果达到限制,则使用channelCheckoutTimeLimit等待连接空闲。如果超时,则抛出AmqpTimeoutException。
重要:
当缓存模式为连接时,不支持队列的自动声明等(请参阅“交换、队列和绑定的自动声明”一节)。
另外,在编写本文时,rabbitmq-client库默认为每个连接(5个线程)创建一个固定的线程池。当使用大量连接时,应该考虑在CachingConnectionFactory上设置自定义执行器。然后,所有连接将使用同一个执行器,并且可以共享其线程。执行程序的线程池应该是无界的,或者为预期的利用率进行适当的设置(通常,每个连接至少有一个线程)。如果在每个连接上创建多个通道,那么池大小将影响并发性,因此最适合使用可变的(或简单缓存)线程池执行器。
重要的是要理解缓存大小(默认情况下)不是限制,而仅仅是可以缓存的通道数量。缓存大小为10时,实际上可以使用任意数量的通道。如果使用超过10个通道,并且它们都返回到缓存中,则10个通道将进入缓存;其余部分将在物理上关闭。
从1.6版开始,默认通道缓存大小从1增加到25。在大容量、多线程、环境中,小缓存意味着高速创建和关闭通道。增加默认缓存大小将避免这种开销。您应该通过RabbitMQ管理UI监视正在使用的通道,如果您看到正在创建和关闭许多通道,则应考虑进一步增加缓存大小。缓存只会按需增长(以适应应用程序的并发性需求),因此此更改不会影响现有的低容量应用程序。
从1.4.2版本开始,CachingConnectionFactory有一个属性channelCheckoutTimeout。当此属性大于零时,channelCacheSize将成为对可以在连接上创建的通道数量的限制。如果达到此限制,调用线程将阻塞,直到通道可用或达到此超时为止,在这种情况下,将抛出AmqpTimeoutException。
警告:框架中使用的通道(例如RabbitTemplate)将可靠地返回到缓存中。如果您在框架之外创建通道(例如,通过直接访问连接并调用createChannel()),您必须可靠地(通过关闭)返回它们(可能在finally块中),以避免通道耗尽。
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
Connection connection = connectionFactory.createConnection();
When using XML, the configuration might look like this:
<bean id="connectionFactory"
class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
<constructor-arg value="somehost"/>
<property name="username" value="guest"/>
<property name="password" value="guest"/>
</bean>
注意:还有一个SingleConnectionFactory实现,它只在框架的单元测试代码中可用。它比CachingConnectionFactory更简单,因为它不缓存通道,但是由于它缺乏性能和弹性,所以不适合在简单测试之外的实际使用。如果由于某种原因需要实现自己的ConnectionFactory, AbstractConnectionFactory基类可能会提供一个很好的起点。
使用rabbit名称空间可以快速方便地创建ConnectionFactory:
<rabbit:connection-factory id="connectionFactory"/>
在大多数情况下,这将是可取的,因为框架可以为您选择最佳的默认值。创建的实例将是CachingConnectionFactory。请记住,通道的默认缓存大小是25。如果希望缓存更多的通道,可以通过channelCacheSize属性设置更大的值。在XML中是这样的:
<bean id="connectionFactory"
class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
<constructor-arg value="somehost"/>
<property name="username" value="guest"/>
<property name="password" value="guest"/>
<property name="channelCacheSize" value="50"/>
</bean>
使用名称空间,您可以添加通道缓存大小属性:
<rabbit:connection-factory
id="connectionFactory" channel-cache-size="50"/>
默认缓存模式是CHANNEL,但您可以将其配置为缓存连接;在本例中,我们使用connection-cache-size:
<rabbit:connection-factory
id="connectionFactory" cache-mode="CONNECTION" connection-cache-size="25"/>
可以使用名称空间提供主机和端口属性:
<rabbit:connection-factory
id="connectionFactory" host="somehost" port="5672"/>
或者,如果在集群环境中运行,使用address属性。
<rabbit:connection-factory
id="connectionFactory" addresses="host1:5672,host2:5672"/>
下面是一个自定义线程工厂的例子,它用rabbitmq-作为线程名的前缀。
<rabbit:connection-factory id="multiHost" virtual-host="/bar" addresses="host1:1234,host2,host3:4567"
thread-factory="tf"
channel-cache-size="10" username="user" password="password" />
<bean id="tf" class="org.springframework.scheduling.concurrent.CustomizableThreadFactory">
<constructor-arg value="rabbitmq-" />
</bean>
从版本1.7开始,为注入AbstractionConnectionFactory提供了ConnectionNameStrategy。生成的名称用于目标RabbitMQ连接的特定于应用程序的标识。如果RabbitMQ服务器支持该连接名,则该连接名将显示在管理UI中。这个值不必是唯一的,也不能用作连接标识符,例如在HTTP API请求中。这个值应该是人类可读的,并且是connection_name键下ClientProperties的一部分。可以用作一个简单的Lambda:
connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION");
ConnectionFactory参数可以用一些逻辑来区分目标连接名。默认情况下,AbstractConnectionFactory的beanName和内部计数器用于生成connection_name。<rabbit:connection-factory>名称空间组件还提供了connection-name-strategy属性。
从1.7.7版本开始,提供了一个AmqpResourceNotAvailableException,当simpleconconnection . createchannel()无法创建通道时抛出该异常,例如,因为通道elmax限制已经达到,缓存中没有可用的通道。这个异常可以在RetryPolicy中使用,以在某些回退之后恢复操作。
Configuring the Underlying Client Connection Factory 配置基础客户端连接工厂
CachingConnectionFactory使用Rabbit客户机ConnectionFactory的实例;在CachingConnectionFactory上设置等效属性时,会传递许多配置属性(例如主机、端口、用户名、密码、requestedHeartBeat、connectionTimeout)。要设置其他属性(例如clientProperties),请定义rabbit factory的一个实例,并使用CachingConnectionFactory的适当构造函数提供对它的引用。
当使用上面描述的名称空间时,在connection-factory属性中提供对已配置工厂的引用。为了方便起见,提供了一个工厂bean来帮助在Spring应用程序上下文中配置连接工厂,下一节将对此进行讨论。
<rabbit:connection-factory
id="connectionFactory" connection-factory="rabbitConnectionFactory"/>
注意:
The 4.0.x版本客户端默认支持自动恢复;虽然与此特性兼容,但Spring AMQP有自己的恢复机制,通常不需要客户机恢复特性。建议禁用amqm -client自动恢复,以避免在代理可用但连接尚未恢复时获得AutoRecoverConnectionNotCurrentlyOpenException s。您可能会注意到这个异常,例如,当在RabbitTemplate中配置了RetryTemplate时,甚至在集群中失败切换到另一个代理时也是如此。由于自动恢复连接在计时器上恢复,因此使用Spring AMQP的恢复机制可以更快地恢复连接。从1.7.1版本开始,Spring AMQP禁用它,除非您显式地创建自己的RabbitMQ连接工厂并将其提供给CachingConnectionFactory。由RabbitConnectionFactoryBean创建的RabbitMQ ConnectionFactory实例也将在缺省情况下禁用该选项。
RabbitConnectionFactoryBean and Configuring SSL
从1.4版开始,提供了一个方便的RabbitConnectionFactoryBean,使用依赖项注入在底层客户端连接工厂上启用方便的SSL属性配置。其他setter只委托给底层工厂。以前,您必须以编程方式配置SSL选项。
<rabbit:connection-factory id="rabbitConnectionFactory"
connection-factory="clientConnectionFactory"
host="${host}"
port="${port}"
virtual-host="${vhost}"
username="${username}" password="${password}" />
<bean id="clientConnectionFactory"
class="org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean">
<property name="useSSL" value="true" />
<property name="sslPropertiesLocation" value="file:/secrets/rabbitSSL.properties"/>
</bean>
有关配置SSL的信息,请参阅RabbitMQ文档。省略密钥存储库和信任存储库配置,以便在没有证书验证的情况下通过SSL连接。密钥和信任存储库配置如下:
sslPropertiesLocation属性是指向包含以下键的属性文件的Spring资源:
keyStore=file:/secret/keycert.p12
trustStore=file:/secret/trustStore
keyStore.passPhrase=secret
trustStore.passPhrase=secret
keyStore和truststore是指向这些存储的Spring资源。通常,该属性文件将由具有读访问权限的操作系统保护。从Spring AMQP 1.5版本开始,可以直接在工厂bean上设置这些属性。如果同时提供离散属性和sslPropertiesLocation,则后者中的属性将覆盖离散值。
Routing Connection Factory
从1.3版本开始,已经介绍了AbstractRoutingConnectionFactory。这提供了一种机制来为几个ConnectionFactory配置映射,并在运行时通过某个lookupKey确定目标ConnectionFactory。通常,实现检查线程绑定上下文。为了方便起见,Spring AMQP提供了SimpleRoutingConnectionFactory,它从SimpleResourceHolder获取当前线程绑定的lookupKey:
<bean id="connectionFactory"
class="org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory">
<property name="targetConnectionFactories">
<map>
<entry key="#{connectionFactory1.virtualHost}" ref="connectionFactory1"/>
<entry key="#{connectionFactory2.virtualHost}" ref="connectionFactory2"/>
</map>
</property>
</bean>
<rabbit:template id="template" connection-factory="connectionFactory" />
public class MyService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void service(String vHost, String payload) {
SimpleResourceHolder.bind(rabbitTemplate.getConnectionFactory(), vHost);
rabbitTemplate.convertAndSend(payload);
SimpleResourceHolder.unbind(rabbitTemplate.getConnectionFactory());
}
}
重要的是在使用资源后解除绑定。有关更多信息,请参见AbstractRoutingConnectionFactory的JavaDocs。
从1.4版开始,RabbitTemplate支持SpEL sendConnectionFactorySelectorExpression和receiveConnectionFactorySelectorExpression属性,这些属性在每个AMQP协议交互操作(send、sendAndReceive、receive或receiveAndReply)上进行评估,并解析为提供的AbstractRoutingConnectionFactory的lookupKey值。可以在表达式中使用Bean引用,比如“@vHostResolver.getVHost(#root)”。对于发送操作,要发送的消息是根评估对象;对于接收操作,queueName是根计算对象。
路由算法是:如果选择器表达式为null,或者被计算为null,或者提供的ConnectionFactory不是AbstractRoutingConnectionFactory的实例,那么一切都像以前一样工作,依赖于提供的ConnectionFactory实现。如果计算结果不是null,则会发生相同的情况,但是该lookupKey没有目标ConnectionFactory, AbstractRoutingConnectionFactory配置lenientFallback = true。当然,在AbstractRoutingConnectionFactory的情况下,它会返回到基于行列式urrentlookupkey()的路由实现。但是,如果lenientFallback = false,则抛出一个IllegalStateException。
命名空间支持还在<rabbit:template>组件上提供了发送-连接-工厂-选择器-表达式和接收-连接-工厂-选择器-表达式属性。
同样,从1.4版本开始,您可以在侦听器容器中配置路由连接工厂。在这种情况下,队列名称列表用作查找键。例如,如果您使用setQueueNames(“foo”、“bar”)配置容器,那么查找键将是“[foo,bar]”(没有空格)。
从1.6.9版本开始,您可以使用侦听器容器上的setLookupKeyQualifier向查找键添加一个限定符。例如,这将启用侦听具有相同名称的队列,但是在不同的虚拟主机中(每个虚拟主机都有一个连接工厂)。
例如,使用查找键限定符foo和侦听队列栏的容器,您将注册目标连接工厂的查找键将是foo[bar]。
Queue Affinity and the LocalizedQueueConnectionFactory 队列关联和LocalizedQueueConnectionFactory
在集群中使用HA队列时,为了获得最佳性能,最好连接到主队列所在的物理代理。而CachingConnectionFactory可以配置多个代理地址;这是故障转移,客户端将尝试按顺序连接。LocalizedQueueConnectionFactory使用管理插件提供的REST API来确定控制了队列的哪个节点。然后,它创建(或从缓存中检索)一个CachingConnectionFactory,该工厂将仅连接到该节点。如果连接失败,则确定新的主节点并由使用者连接到它。LocalizedQueueConnectionFactory配置了一个默认连接工厂,以防无法确定队列的物理位置,在这种情况下,它将正常连接到集群。
LocalizedQueueConnectionFactory是一个RoutingConnectionFactory, SimpleMessageListenerContainer使用队列名称作为查找键,正如上面“路由连接工厂”一节中讨论的那样。
注意:由于这个原因(使用队列名称进行查找),LocalizedQueueConnectionFactory只能在容器被配置为侦听单个队列时使用。
注意:必须在每个节点上启用RabbitMQ管理插件。
谨慎:这个连接工厂适用于长生命周期的连接,比如SimpleMessageListenerContainer使用的连接。由于在建立连接之前调用REST API的开销,所以不打算使用短连接,比如使用RabbitTemplate。此外,对于发布操作,队列是未知的,而且消息无论如何都会发布给所有集群成员,因此查找节点的逻辑没有什么价值。
下面是一个配置示例,使用Spring Boot的RabbitProperties来配置工厂:
@Autowired
private RabbitProperties props;
private final String[] adminUris = { "http://host1:15672", "http://host2:15672" };
private final String[] nodes = { "rabbit@host1", "rabbit@host2" };
@Bean
public ConnectionFactory defaultConnectionFactory() {
CachingConnectionFactory cf = new CachingConnectionFactory();
cf.setAddresses(this.props.getAddresses());
cf.setUsername(this.props.getUsername());
cf.setPassword(this.props.getPassword());
cf.setVirtualHost(this.props.getVirtualHost());
return cf;
}
@Bean
public ConnectionFactory queueAffinityCF(
@Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) {
return new LocalizedQueueConnectionFactory(defaultCF,
StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()),
this.adminUris, this.nodes,
this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(),
false, null);
}
注意,前三个参数是地址、adminuri和节点的数组。当容器试图连接到队列时,它确定队列控制在哪个节点上,并连接到相同数组位置的地址。
Publisher Confirms and Returns
通过将CachingConnectionFactory的publisherconfirm和publisherReturns属性分别设置为“true”,可以支持已确认和返回的消息。
设置了这些选项后,工厂创建的通道s被包装在PublisherCallbackChannel中,用于方便回调。当获得这样一个通道时,客户机可以注册PPublisherCallbackChannel.Listener。PublisherCallbackChannel实现包含将确认/返回路由到适当侦听器的逻辑。下面几节将进一步解释这些特性。
提示:有关更多的背景信息,请参见RabbitMQ团队的博客文章《介绍Publisher》。
Logging Channel Close Events
在1.5版中引入了一种允许用户控制日志记录级别的机制。
CachingConnectionFactory使用默认策略记录通道关闭,如下所示:
- 正常通道关闭(200 OK)没有被记录。
- 如果通道由于被动队列声明失败而关闭,则会将其记录在调试级别。
- If a channel is closed because the basic.consume is refused due to an exclusive consumer condition, it is logged at INFO level.
- 所有其他日志记录在错误级别。
要修改此行为,请在CachingConnectionFactory的closeExceptionLogger属性中注入一个定制的ConditionalExceptionLogger。
Also see the section called “Consumer Events”.
Runtime Cache Properties
从1.6版开始,CachingConnectionFactory现在通过getCacheProperties()方法提供缓存统计信息。这些统计数据可用于在生产中优化缓存。例如,可以使用高水迹来确定是否应该增加缓存大小。如果它等于缓存大小,您可能需要考虑进一步增加。
Table 3.1. Cache properties for CacheMode.CHANNEL
Property | Meaning |
---|---|
|
ConnectionNameStrategy生成的连接的名称。 |
|
当前配置的允许空闲的最大通道。 |
| 连接的本地端口(如果可用)。这可以用来关联RabbitMQ管理UI上的连接/通道。 |
| 当前空闲(缓存)的事务通道的数量。 |
| 当前空闲(缓存)的非事务性通道的数量。 |
|
并发空闲(缓存)的事务通道的最大数量。 |
|
并发空闲(缓存)的非事务性通道的最大数量。
|
Table 3.2. Cache properties for CacheMode.CONNECTION
Property | Meaning |
---|---|
|
ConnectionNameStrategy生成的连接的名称。 |
|
表示到代理的连接的连接对象的数量。 |
|
当前配置的允许空闲的最大通道。 |
|
当前配置的允许空闲的最大连接。 |
|
当前空闲的连接数。 |
|
并发空闲的最大连接数。 |
| 此连接当前空闲(缓存)的事务通道的数量。属性名的localPort部分可用于与RabbitMQ管理UI上的连接/通道关联。 |
| 此连接当前空闲(缓存)的非事务性通道的数量。属性名的localPort部分可用于与RabbitMQ管理UI上的连接/通道关联。 |
| 并发空闲(缓存)的事务通道的最大数量。属性名的localPort部分可用于与RabbitMQ管理UI上的连接/通道关联。 |
| 并发空闲(缓存)的非事务性通道的最大数量。属性名的localPort部分可用于与RabbitMQ管理UI上的连接/通道关联。 |
cacheMode属性(还包括通道或连接)。
Figure 3.1. JVisualVM Example
RabbitMQ Automatic Connection/Topology recovery RabbitMQ自动连接/拓扑恢复
自Spring AMQP的第一个版本以来,该框架在代理失败时提供了自己的连接和通道恢复。
此外,正如3.1.10节“配置代理”中讨论的,当重新建立连接时,RabbitAdmin将重新声明任何基础设施bean(队列等)。因此,它不依赖于现在由amqp-client库提供的自动恢复。Spring AMQP现在使用4.0.x版本的amqp-client的,默认情况下启用了自动恢复功能。如果您愿意,Spring AMQP仍然可以使用它自己的恢复机制,在客户机中禁用它(通过将底层RabbitMQ connectionFactory上的automaticRecoveryEnabled属性设置为false)。然而,该框架完全兼容自动恢复功能。这意味着您在代码中创建的任何消费者(可能通过RabbitTemplate.execute())都可以自动恢复。
3.1.3 Adding Custom Client Connection Properties 添加自定义客户端连接属性
CachingConnectionFactory现在允许您访问底层连接工厂,例如,允许设置自定义客户端属性:
connectionFactory.getRabbitConnectionFactory().getClientProperties().put("foo", "bar");
在查看连接时,这些属性将出现在RabbitMQ管理UI中。
3.1.4 AmqpTemplate
Introduction
与Spring框架和相关项目提供的许多其他高级抽象一样,Spring AMQP提供了一个“模板”,它扮演着核心角色。定义主要操作的接口称为AmqpTemplate。这些操作涵盖了发送和接收消息的一般行为。换句话说,它们对于任何实现都不是惟一的,因此名称中有“AMQP”。另一方面,该接口的一些实现与AMQP协议的实现绑定在一起。与JMS本身是接口级API不同,AMQP是一个线级协议。该协议的实现提供了自己的客户机库,因此模板接口的每个实现都依赖于特定的客户机库。目前,只有一个实现:RabbitTemplate。在接下来的示例中,您将经常看到“AmqpTemplate”的用法,但是当您查看配置示例,或者调用模板实例化和/或setter的任何代码摘录时,您将看到实现类型(例如,“RabbitTemplate”)。
如上所述,AmqpTemplate接口定义了发送和接收消息的所有基本操作。我们将在下面的两部分中分别探讨消息发送和接收。
See also the section called “AsyncRabbitTemplate”.
Adding Retry Capabilities 添加重试功能
从1.3版开始,现在可以配置RabbitTemplate,使用RetryTemplate帮助处理代理连接问题。有关完整信息,请参阅spring-retry项目;下面只是使用指数回退策略和默认SimpleRetryPolicy的一个例子,默认SimpleRetryPolicy在将异常抛出给调用者之前会尝试三次。
Using the XML namespace:
<rabbit:template id="template" connection-factory="connectionFactory" retry-template="retryTemplate"/>
<bean id="retryTemplate" class="org.springframework.retry.support.RetryTemplate">
<property name="backOffPolicy">
<bean class="org.springframework.retry.backoff.ExponentialBackOffPolicy">
<property name="initialInterval" value="500" />
<property name="multiplier" value="10.0" />
<property name="maxInterval" value="10000" />
</bean>
</property>
</bean>
Using @Configuration
:
@Bean
public AmqpTemplate rabbitTemplate();
RabbitTemplate template = new RabbitTemplate(connectionFactory());
RetryTemplate retryTemplate = new RetryTemplate();
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(500);
backOffPolicy.setMultiplier(10.0);
backOffPolicy.setMaxInterval(10000);
retryTemplate.setBackOffPolicy(backOffPolicy);
template.setRetryTemplate(retryTemplate);
return template;
}
从版本1.4开始,除了retryTemplate属性之外,RabbitTemplate还支持recoveryCallback选项。它用作RetryTemplate的第二个参数。执行(RetryCallback<T, E> RetryCallback, RecoveryCallback<T> RecoveryCallback)。
注意:RecoveryCallback有一定的限制,因为retry上下文只包含lastThrowable字段。对于更复杂的用例,应该使用外部RetryTemplate,以便通过上下文的属性向RecoveryCallback传递额外的信息:
retryTemplate.execute(
new RetryCallback<Object, Exception>() {
@Override
public Object doWithRetry(RetryContext context) throws Exception {
context.setAttribute("message", message);
return rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
}, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
Object message = context.getAttribute("message");
Throwable t = context.getLastThrowable();
// Do something with message
return null;
}
});
}
在本例中,您不会将RetryTemplate注入到RabbitTemplate中。
Publisher Confirms and Returns
AmqpTemplate的RabbitTemplate实现支持Publisher确认和返回。
对于返回的消息,模板的mandatory属性必须设置为true,或者对于特定的消息,强制表达式的计算值必须为true。这个特性需要一个CachingConnectionFactory,它将publisherReturns属性设置为true(请参阅“Publisher confirm and Returns”一节)。通过注册一个RabbitTemplate将返回发送给客户机。通过调用setReturnCallback(ReturnCallback回调)来返回回调。回调必须实现这个方法:
void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey);
每个RabbitTemplate只支持一个ReturnCallback。请参见“回复超时”一节。
对于发布者confirm(也称为发布者确认),模板需要一个CachingConnectionFactory,该工厂将其publisherconfirm属性设置为true。确认通过注册一个RabbitTemplate.ConfirmCallback。通过调用setConfirmCallback(ConfirmCallback回调)来调用ConfirmCallback。回调必须实现这个方法:
void confirm(CorrelationData correlationData, boolean ack, String cause);
CorrelationData是客户机在发送原始消息时提供的对象。ack对ack为真,对nack为假。对于nack s,如果生成nack时可以找到原因,则原因可能包含nack的原因。例如,当向不存在的交换器发送消息时。在这种情况下,代理关闭通道;闭包的原因包括在原因中。cause是在1.4版中添加的。
RabbitTemplate只支持一个ConfirmCallback。
注意:当rabbit 模板发送操作完成时,通道关闭;这将排除在连接工厂缓存已满时接收确认或返回(当缓存中有空间时,通道没有物理关闭,而返回/确认将正常进行)。当缓存已满时,框架将关闭延迟至多5秒,以便有时间接收确认/返回。
当使用confirm时,当收到最后一个confirm时,通道将被关闭。当只使用返回值时,通道将在整个5秒内保持打开状态。通常建议将连接工厂的channelCacheSize设置为足够大的值,以便将发布消息的通道返回给缓存而不是关闭。您可以使用RabbitMQ管理插件监视通道使用情况;如果您看到通道被快速打开/关闭,您应该考虑增加缓存大小,以减少服务器上的开销。
Messaging integration 信息集成
从基于RabbitTemplate的1.4版RabbitMessagingTemplate开始,提供了与Spring框架消息传递抽象(即org.springframework.messaging.Message)的集成。这允许您使用spring-messaging Message<?>抽象。这个抽象被其他Spring项目使用,比如Spring Integration和Spring的STOMP支持。
MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter();
amqpMessageConverter.setPayloadConverter(myPayloadConverter);
rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter);
Validated User Id
从1.6版开始,模板现在支持用户id表达式(使用Java配置时支持userIdExpression)。如果发送了一条消息,则在计算此表达式之后设置用户id属性(如果尚未设置)。求值的根对象是要发送的消息。
Examples:
<rabbit:template ... user-id-expression="'guest'" />
<rabbit:template ... user-id-expression="@myConnectionFactory.username" />
第一个例子是文字表达式;第二种方法从应用程序上下文中的连接工厂bean获取username属性。
3.1.5 Sending messages
Introduction
发送消息时,可以使用以下任何一种方法:
void send(Message message) throws AmqpException;
void send(String routingKey, Message message) throws AmqpException;
void send(String exchange, String routingKey, Message message) throws AmqpException;
我们可以从上面列出的最后一个方法开始讨论,因为它实际上是最显式的。它允许在运行时提供AMQP交换名和路由键。最后一个参数是负责实际创建消息实例的回调函数。使用这种方法发送消息的一个例子可能是这样的:
amqpTemplate.send("marketData.topic", "quotes.nasdaq.FOO",
new Message("12.34".getBytes(), someProperties));
如果您计划在大部分时间或全部时间使用模板实例发送到同一个exchange,则可以在模板本身上设置“exchange”属性。在这种情况下,可以使用上面列出的第二种方法。下面的例子在功能上与前面的例子相同:
amqpTemplate.setExchange("marketData.topic");
amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties));
如果模板上同时设置了“exchange”和“routingKey”属性,则可以使用只接受消息的方法:
amqpTemplate.setExchange("marketData.topic");
amqpTemplate.setRoutingKey("quotes.nasdaq.FOO");
amqpTemplate.send(new Message("12.34".getBytes(), someProperties));
考虑exchange和路由键属性的更好方法是显式方法参数总是覆盖模板的默认值。事实上,即使您没有显式地在模板上设置这些属性,也总是存在默认值。在这两种情况下,默认值都是空字符串,但这实际上是一个合理的默认值。就路由键而言,它并不总是必须放在首位(例如,一个扇出交换)。此外,队列可以绑定到具有空字符串的交换。这些都是依赖于模板的路由键属性的默认空字符串值的合法场景。就交换名称而言,空字符串非常常用,因为AMQP规范将“缺省交换”定义为没有名称。由于所有队列都使用其名称作为绑定值自动绑定到缺省交换器(即直接交换器),因此可以使用上面的第二个方法通过缺省交换器向任何队列发送简单的点对点消息。只需提供队列名称作为“routingKey”—在运行时提供方法参数即可:
RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange
template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties));
或者,如果您希望创建一个模板,主要用于发布或仅用于发布到单个队列,那么下面的操作非常合理:
RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange
template.setRoutingKey("queue.helloWorld"); // but we'll always send to this Queue
template.send(new Message("Hello World".getBytes(), someProperties));
Message Builder API
从版本1.3开始,MessageBuilder和MessagePropertiesBuilder提供了一个消息构建器API;它们提供了一种方便的“流畅”方法来创建消息或消息属性:
Message message = MessageBuilder.withBody("foo".getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
.setMessageId("123")
.setHeader("bar", "baz")
.build();
or
MessageProperties props = MessagePropertiesBuilder.newInstance()
.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
.setMessageId("123")
.setHeader("bar", "baz")
.build();
Message message = MessageBuilder.withBody("foo".getBytes())
.andProperties(props)
.build();
可以设置MessageProperties上定义的每个属性。其他方法包括setHeader(String key, String value)、removeHeader(String key)、removeHeaders()和copyProperties(MessageProperties properties)。每个属性设置方法都有一个set*IfAbsent()变体。在存在默认初始值的情况下,方法名为set*IfAbsentOrDefault()。
提供了五种静态方法来创建初始消息生成器:
public static MessageBuilder withBody(byte[] body)
public static MessageBuilder withClonedBody(byte[] body)
public static MessageBuilder withBody(byte[] body, int from, int to)
public static MessageBuilder fromMessage(Message message)
public static MessageBuilder fromClonedMessage(Message message)
- 构建器创建的消息将有一个直接引用该参数的主体。
- 构建器创建的消息的主体是一个新数组,其中包含参数中的字节副本。
- 构建器创建的消息将具有一个新的数组主体,其中包含来自参数的字节范围。有关详细信息,请参见Arrays.copyOfRange()。
- 构建器创建的消息将有一个直接引用参数主体的主体。参数的属性被复制到一个新的MessageProperties对象。
- 构建器创建的消息将有一个主体,它是一个包含参数主体副本的新数组。参数的属性被复制到一个新的MessageProperties对象。
public static MessagePropertiesBuilder newInstance()
public static MessagePropertiesBuilder fromProperties(MessageProperties properties)
public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties)
- 使用默认值初始化一个新的message properties对象。
- 使用提供的properties对象初始化构建器,build()将返回。
- 参数的属性被复制到一个新的MessageProperties对象。
使用AmqpTemplate的RabbitTemplate实现,每个send()方法都有一个重载版本,该版本接受一个额外的CorrelationData对象。当publisher确认已启用时,此对象将在第3.1.4节“AmqpTemplate”中描述的回调中返回。这允许发送方将确认(ack或nack)与发送的消息关联起来。
从1.6.7版本开始,引入了CorrelationAwareMessagePostProcessor接口,允许在消息转换之后修改相关数据:
Message postProcessMessage(Message message, Correlation correlation);
从1.6.7版本开始,还提供了一个新的回调接口CorrelationDataPostProcessor;这在所有MessagePostProcessor(在send()方法中提供)和setBeforePublishPostProcessors()中提供的MessagePostProcessor()之后调用。实现可以更新或替换send()方法中提供的相关数据(如果有的话)。消息和原始的相关数据(如果有的话)作为参数提供。
CorrelationData postProcess(Message message, CorrelationData correlationData);
Publisher Returns
当模板的强制属性为true时,返回的消息由第3.1.4节中描述的回调函数“AmqpTemplate”提供。
从1.4版开始,RabbitTemplate支持SpEL mandatoryExpression属性,该属性作为根评估对象根据每个请求消息进行评估,并解析为一个布尔值。可以在表达式中使用Bean引用,比如“@myBean.isMandatory(#root)”。
发布者返回值也可以由RabbitTemplate在发送和接收操作中在内部使用。有关更多信息,请参见“回复超时”一节。
Batching
从1.4.2版开始,介绍了BatchingRabbitTemplate。这是RabbitTemplate的子类,带有一个覆盖的send方法,该方法根据BatchingStrategy对消息进行批处理;只有当批处理完成时,才将消息发送到RabbitMQ。
public interface BatchingStrategy {
MessageBatch addToBatch(String exchange, String routingKey, Message message);
Date nextRelease();
Collection<MessageBatch> releaseBatches();
}
谨慎:批量数据保存在内存中;在系统发生故障时,未发送的消息可能会丢失。
提供了一种简单的批处理策略。它支持向单个交换/路由键发送消息。它有属性:
batchSize
批处理中发送前的消息数量
- bufferLimit 批处理消息的最大大小;如果超过此值,将抢占批处理大小,并导致发送部分批处理
timeout
当没有向批处理添加消息的新活动时,发送部分批处理的时间
SimpleBatchingStrategy通过在每个嵌入的消息前面加上4字节的二进制长度来格式化批处理。通过将springBatchFormat message属性设置为lengthHeader4,可以将此消息传递给接收系统。
重要:批处理消息由侦听器容器自动反批处理(使用springBatchFormat消息头)。拒绝批处理中的任何消息都会导致整个批处理被拒绝。
3.1.6 Receiving messages
Introduction
消息接收总是比发送稍微复杂一些。接收消息有两种方法。更简单的方法是使用轮询方法调用一次轮询一条消息。更复杂但更常见的方法是注册一个侦听器,该侦听器将按需异步接收消息。我们将在下两个小节中查看每种方法的示例。
Polling Consumer
默认情况下,如果没有可用的消息,则立即返回null;没有阻塞。从1.5版开始,您现在可以设置一个receiveTimeout(以毫秒为单位),receive方法将阻塞最长时间,等待消息。小于零的值表示无限期阻塞(或至少在与代理的连接丢失之前)。Version 1.6引入了各种receive方法,允许在每次调用时传入超时。
谨慎:由于receive操作为每个消息创建一个新的QueueingConsumer,因此这种技术并不真正适合于大容量环境;考虑为这些用例使用异步使用者,或者接收超时为零。
有四种简单的接收方法可用。与发送端上的Exchange一样,有一个方法要求直接在模板本身上设置缺省队列属性,还有一个方法在运行时接受队列参数。版本1.6引入了一些变体来接受timeoutMillis,以便在每个请求的基础上覆盖receiveTimeout。
Message receive() throws AmqpException;
Message receive(String queueName) throws AmqpException;
Message receive(long timeoutMillis) throws AmqpException;
Message receive(String queueName, long timeoutMillis) throws AmqpException;
就像在发送消息的情况下,AmqpTemplate有一些方便的方法来接收pojo而不是消息实例,实现将提供一种自定义MessageConverter的方法,用于创建返回的对象:
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
Message receiveAndConvert(long timeoutMillis) throws AmqpException;
Message receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException;
与sendAndReceive方法类似,从1.3版本开始,AmqpTemplate有几个方便的receiveAndReply方法,用于同步接收、处理和回复消息:
<R, S> boolean receiveAndReply(ReceiveAndReplyCallback<R, S> callback)
throws AmqpException;
<R, S> boolean receiveAndReply(String queueName, ReceiveAndReplyCallback<R, S> callback)
throws AmqpException;
<R, S> boolean receiveAndReply(ReceiveAndReplyCallback<R, S> callback,
String replyExchange, String replyRoutingKey) throws AmqpException;
<R, S> boolean receiveAndReply(String queueName, ReceiveAndReplyCallback<R, S> callback,
String replyExchange, String replyRoutingKey) throws AmqpException;
<R, S> boolean receiveAndReply(ReceiveAndReplyCallback<R, S> callback,
ReplyToAddressCallback<S> replyToAddressCallback) throws AmqpException;
<R, S> boolean receiveAndReply(String queueName, ReceiveAndReplyCallback<R, S> callback,
ReplyToAddressCallback<S> replyToAddressCallback) throws AmqpException;
AmqpTemplate实现负责接收和应答阶段。在大多数情况下,您应该只提供ReceiveAndReplyCallback的实现,以便为接收到的消息执行一些业务逻辑,并在需要时构建一个reply对象或消息。注意,ReceiveAndReplyCallback可能返回null。在这种情况下,没有发送和接收应答,并且与receive方法类似。这允许将相同的队列用于混合消息,其中一些消息可能不需要回复。
只有当提供的回调不是ReceiveAndReplyMessageCallback的实例时,才会应用自动消息(请求和应答)转换——该回调提供了原始消息交换契约。
ReplyToAddressCallback对于需要定制逻辑在运行时根据接收到的消息和来自ReceiveAndReplyCallback的回复确定replyTo地址的情况非常有用。默认情况下,请求消息中的replyTo信息用于路由应答。
下面是一个基于pojo的接收和回复示例…
boolean received =
this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback<Order, Invoice>() {
public Invoice handle(Order order) {
return processOrder(order);
}
});
if (received) {
log.info("We received an order!");
}
Asynchronous Consumer
重要:Spring AMQP还通过使用@RabbitListener注释支持带注释的侦听器端点,并提供一个开放的基础设施以编程方式注册端点。这是迄今为止设置异步使用者最方便的方法,有关详细信息,请参阅“注释驱动的侦听器端点”一节。
Message Listener
对于异步消息接收,需要一个专用组件(不是AmqpTemplate)。该组件是消息使用回调的容器。稍后我们将研究容器及其属性,但首先我们应该研究回调,因为应用程序代码将在回调中与消息传递系统集成。有几个选项可以让回调从MessageListener接口的实现开始:
public interface MessageListener {
void onMessage(Message message);
}
如果回调逻辑由于任何原因依赖于AMQP通道实例,则可以使用ChannelAwareMessageListener。它看起来很像,但有一个额外的参数:
public interface ChannelAwareMessageListener {
void onMessage(Message message, Channel channel) throws Exception;
}
MessageListenerAdapter
如果希望在应用程序逻辑和消息传递API之间保持更严格的分离,可以依赖于框架提供的适配器实现。这通常被称为“消息驱动的POJO”支持。当使用适配器时,您只需要提供对适配器本身应该调用的实例的引用。
MessageListenerAdapter listener = new MessageListenerAdapter(somePojo);
listener.setDefaultListenerMethod("myMethod");
您可以子类化适配器并提供getListenerMethodName()的实现,以便根据消息动态选择不同的方法。该方法有两个参数,originalMessage和extractedMessage,后者是任何转换的结果。默认情况下,配置了SimpleMessageConverter;有关更多信息和其他可用转换器的信息,请参阅名为“SimpleMessageConverter”的部分。
从1.4.2版本开始,原始消息具有consumerQueue和consumerTag属性,可用于确定消息是从哪个队列接收的。
从1.5版开始,您可以将使用者队列/标记映射配置为方法名,以便动态选择要调用的方法。如果映射中没有条目,则返回到默认侦听器方法。
Container
现在您已经看到了用于消息监听回调的各种选项,我们可以将注意力转向容器。基本上,容器处理“活动”职责,以便侦听器回调可以保持被动。容器是“生命周期”组件的一个例子。它提供了启动和停止方法。在配置容器时,您实际上是在架起AMQP队列和MessageListener实例之间的桥梁。
您必须提供对ConnectionFactory的引用,以及侦听器应该从中消费消息的队列名称或队列实例。下面是使用默认实现SimpleMessageListenerContainer的最基本示例:
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(rabbitConnectionFactory);
container.setQueueNames("some.queue");
container.setMessageListener(new MessageListenerAdapter(somePojo));
作为一个“活动”组件,最常见的做法是使用bean定义创建侦听器容器,以便它可以在后台运行。这可以通过XML实现:
<rabbit:listener-container connection-factory="rabbitConnectionFactory">
<rabbit:listener queues="some.queue" ref="somePojo" method="handle"/>
</rabbit:listener-container>
或者,您可能更喜欢使用@Configuration样式,它看起来非常类似于上面的实际代码片段:
@Configuration
public class ExampleAmqpConfiguration {
@Bean
public SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(rabbitConnectionFactory());
container.setQueueName("some.queue");
container.setMessageListener(exampleListener());
return container;
}
@Bean
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory connectionFactory =
new CachingConnectionFactory("localhost");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
return connectionFactory;
}
@Bean
public MessageListener exampleListener() {
return new MessageListener() {
public void onMessage(Message message) {
System.out.println("received: " + message);
}
};
}
}
从RabbitMQ 3.2版本开始,代理现在支持使用者优先级(请参阅使用RabbitMQ的使用者优先级)。这是通过在使用者上设置x优先级参数来实现的。SimpleMessageListenerContainer现在支持设置消费者参数:
container.setConsumerArguments(Collections.
<String, Object> singletonMap("x-priority", Integer.valueOf(10)));
为了方便起见,名称空间在listener元素上提供了priority属性:
<rabbit:listener-container connection-factory="rabbitConnectionFactory">
<rabbit:listener queues="some.queue" ref="somePojo" method="handle" priority="10" />
</rabbit:listener-container>
从版本1.3开始,可以在运行时修改容器正在监听的队列;参见3.1.18节“侦听器容器队列”。
auto-delete Queues
当容器被配置为侦听自动删除队列,或者队列具有x-expires选项,或者代理上配置了生存时间策略,当容器停止时,代理将删除队列(最后一个使用者被取消)。在版本1.3之前,由于队列丢失,容器无法重新启动;RabbitAdmin只在连接关闭/打开时自动重新声明队列等,而在容器停止/启动时不会发生这种情况。
从1.3版本开始,容器现在将使用RabbitAdmin在启动期间重新声明任何丢失的队列。您还可以使用条件声明(称为“条件声明”的部分)和自动启动=“false”admin来延迟队列声明,直到容器启动。
<rabbit:queue id="otherAnon" declared-by="containerAdmin" />
<rabbit:direct-exchange name="otherExchange" auto-delete="true" declared-by="containerAdmin">
<rabbit:bindings>
<rabbit:binding queue="otherAnon" key="otherAnon" />
</rabbit:bindings>
</rabbit:direct-exchange>
<rabbit:listener-container id="container2" auto-startup="false">
<rabbit:listener id="listener2" ref="foo" queues="otherAnon" admin="containerAdmin" />
</rabbit:listener-container>
<rabbit:admin id="containerAdmin" connection-factory="rabbitConnectionFactory"
auto-startup="false" />
在本例中,队列和交换由containmin声明,它具有auto-startup="false",因此在上下文初始化期间不会声明元素。同样,容器也不是出于相同的原因启动的。当容器稍后启动时,它使用对contain根除min的引用来声明元素。
Batched Messages
批处理消息由侦听器容器自动反批处理(使用springBatchFormat消息头)。拒绝批处理中的任何消息都会导致整个批处理被拒绝。有关批处理的更多信息,请参见“批处理”一节。
Consumer Events
从1.5版本开始,SimpleMessageListenerContainer将在侦听器(消费者)遭遇某种失败时发布应用程序事件。事件ListenerContainerConsumerFailedEvent具有以下属性:
container
- 消费者遇到问题的侦听器容器。
reason
- 失败的文本原因。
fatal
- 指示失败是否致命的布尔值;对于非致命异常,容器将根据retryInterval尝试重新启动使用者。
throwable
- theThrowable
that was caught.
可以通过实现ApplicationListener<ListenerContainerConsumerFailedEvent>来使用这些事件。
当concurrentConsumers大于1时,所有使用者将发布系统范围的事件(例如连接失败)。
如果使用者失败,因为它的队列在缺省情况下是专用的,并且发布事件,那么将发出警告日志。要更改此日志记录行为,请在SimpleMessageListenerContainer的exclusive veconsumerexceptionlogger属性中提供一个定制的ConditionalExceptionLogger。请参见“记录通道关闭事件”一节。
致命错误总是记录在错误级别;这是不可修改的。
在容器生命周期的不同阶段还发布了其他几个事件:
AsyncConsumerStartedEvent
(when the consumer is started)AsyncConsumerRestartedEvent
(when the consumer is restarted after a failure -SimpleMessageListenerContainer
only)AsyncConsumerTerminatedEvent
(when a consumer is stopped normally)AsyncConsumerStoppedEvent
(when the consumer is stopped -SimpleMessageListenerContainer
only)ConsumeOkEvent
(when aconsumeOk
is received from the broker, contains the queue name andconsumerTag
)ListenerContainerIdleEvent
(see the section called “Detecting Idle Asynchronous Consumers”)
Consumer Tags
从1.4.5版本开始,您现在可以提供一种策略来生成消费者标记。默认情况下,consumer标记将由代理生成。
public interface ConsumerTagStrategy {
String createConsumerTag(String queue);
}
队列是可用的,因此可以(可选地)在标记中使用它。
See Section 3.1.15, “Message Listener Container Configuration”.
Annotation-driven Listener Endpoints
Introduction
从1.4版本开始,异步接收消息的最简单方法是使用带注释的侦听器端点基础设施。简而言之,它允许您将托管bean的方法公开为Rabbit 侦听器端点。
@Component
public class MyService {
@RabbitListener(queues = "myQueue")
public void processOrder(String data) {
...
}
}
上面示例的思想是,无论何时在org.springframework.amqp.core.Queue上有消息可用。相应地调用processOrder方法(在本例中,使用消息的有效负载)。
带注释的端点基础设施使用RabbitListenerContainerFactory在后台为每个带注释的方法创建一个消息侦听器容器。
在上面的示例中,myQueue必须已经存在,并被绑定到某个exchange。从1.5.0版本开始,只要应用程序上下文中存在一个RabbitAdmin,就可以自动声明和绑定队列。
可以为注释属性(队列等)指定属性占位符(${some.property})或SpEL表达式(#{someExpression})。有关为什么可能使用SpEL而不是属性占位符的示例,请参阅名为“侦听多个队列”的部分。
@Component
public class MyService {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "myQueue", durable = "true"),
exchange = @Exchange(value = "auto.exch", ignoreDeclarationExceptions = "true"),
key = "orderRoutingKey")
)
public void processOrder(String data) {
...
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue,
exchange = @Exchange(value = "auto.exch"),
key = "invoiceRoutingKey")
)
public void processInvoice(String data) {
...
}
}
在第一个示例中,如果需要,队列myQueue将与交换一起自动(持久)声明,并使用路由键绑定到交换。在第二个示例中,将声明和绑定一个匿名(排他的、自动删除的)队列。可以提供多个QueueBinding条目,允许侦听器侦听多个队列。
这种机制只支持DIRECT、FANOUT、TOPIC和header、exchange类型。当需要更高级的配置时,使用普通的@Bean定义。
注意第一个例子中的ignoredeclaration异常。例如,这允许绑定到可能具有不同设置(例如内部设置)的现有交换器。默认情况下,现有交换器的属性必须匹配。
从1.6版开始,现在可以在@QueueBinding注释中为队列、交换和绑定指定参数。例如:
@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")
})
)
public String handleWithHeadersExchange(String foo) {
...
}
注意,x-message-ttl参数为队列设置为10秒。由于参数类型不是字符串,我们必须指定它的类型;在本例中是Integer。与所有此类声明一样,如果队列已经存在,则参数必须与队列上的参数匹配。对于报头交换,我们设置绑定参数来匹配报头foo设置为bar的消息,报头baz必须带有任何值。x-match参数意味着必须满足这两个条件。
参数名、值和类型可以是属性占位符(${…})或SpEL表达式(#{…})。名称必须解析为字符串;type的表达式必须解析为类或类的完全限定名。该值必须解析为可以由DefaultConversionService转换为类型的值(如上面示例中的x-message-ttl)。
如果名称解析为null或空字符串,则忽略@参数。
Meta-Annotations
有时,您可能希望对多个侦听器使用相同的配置。为了减少样板配置,您可以使用元注释来创建您自己的侦听器注释:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@RabbitListener(bindings = @QueueBinding(
value = @Queue,
exchange = @Exchange(value = "metaFanout", type = ExchangeTypes.FANOUT)))
public @interface MyAnonFanoutListener {
}
public class MetaListener {
@MyAnonFanoutListener
public void handle1(String foo) {
...
}
@MyAnonFanoutListener
public void handle2(String foo) {
...
}
}
在本例中,由@MyAnonFanoutListener 注释创建的每个侦听器都将一个匿名的自动删除队列绑定到fanout exchange metaFanout。元注释机制很简单,因为不检查用户定义的注释上的属性——因此不能覆盖来自元注释的设置。当需要更高级的配置时,使用普通的@Bean定义。
Enable Listener Endpoint Annotations
要启用对@RabbitListener注释的支持,请将@EnableRabbit添加到您的@Configuration类之一。
@Configuration
@EnableRabbit
public class AppConfig {
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setConcurrentConsumers(3);
factory.setMaxConcurrentConsumers(10);
return factory;
}
}
默认情况下,基础设施寻找一个名为rabbitListenerContainerFactory的bean作为工厂用于创建消息侦听器容器的源。在本例中,忽略RabbitMQ基础设施设置,processOrder方法可以通过核心轮询大小为3个线程和最大池大小为10个线程来调用。
如果喜欢XML配置,可以使用<rabbit:annotation-driven>元素。
<rabbit:annotation-driven/>
<bean id="rabbitListenerContainerFactory"
class="org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="concurrentConsumers" value="3"/>
<property name="maxConcurrentConsumers" value="10"/>
</bean>
Message Conversion for Annotated Methods
在调用侦听器之前,管道中有两个转换步骤。第一种方法使用MessageConverter将传入的Spring AMQP消息转换为Spring消息消息。当调用目标方法时,如果需要,消息有效负载将转换为方法参数类型。
第一步的默认MessageConverter是一个Spring AMQP SimpleMessageConverter,它处理到字符串和可序列化的对象的转换。;所有其他的都保留为byte[]。在接下来的讨论中,我们将其称为消息转换器。
第二步的默认转换器是GenericMessageConverter,它将委托给转换服务(DefaultFormattingConversionService的实例)。在下面的讨论中,我们将其称为方法参数转换器。
要更改消息转换器,只需将其作为属性添加到容器工厂bean:
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
...
factory.setMessageConverter(new Jackson2JsonMessageConverter());
...
return factory;
}
这配置了一个Jackson2转换器,它期望头信息能够指导转换。
您还可以考虑ContentTypeDelegatingMessageConverter,它可以处理不同内容类型的转换。
在大多数情况下,没有必要自定义方法参数转换器,除非您想使用自定义的ConversionService。
在1.6之前的版本中,必须在消息头中提供转换JSON的类型信息,或者需要自定义类映射器。从version 1.6开始,如果没有类型信息头,则可以从目标方法参数推断类型。
这种类型推断只适用于方法级别的@RabbitListener。
有关更多信息,请参见“Jackson2JsonMessageConverter”一节。
如果你想自定义方法参数转换器,你可以这样做:
@Configuration
@EnableRabbit
public class AppConfig implements RabbitListenerConfigurer {
...
@Bean
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(new GenericMessageConverter(myConversionService()));
return factory;
}
@Bean
public ConversionService myConversionService() {
DefaultConversionService conv = new DefaultConversionService();
conv.addConverter(mySpecialConverter());
return conv;
}
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
}
...
}
重要:对于多方法监听器(参见“多方法监听器”一节),方法的选择基于消息转换后消息的有效负载;方法参数转换器只有在方法被选中之后才会被调用。
Programmatic Endpoint Registration 编程端点注册
RabbitListenerEndpoint提供了一个Rabbit端点的模型,并负责为该模型配置容器。除了RabbitListener注释检测到的端点之外,该基础设施还允许您以编程方式配置端点。
@Configuration
@EnableRabbit
public class AppConfig implements RabbitListenerConfigurer {
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
endpoint.setQueueNames("anotherQueue");
endpoint.setMessageListener(message -> {
// processing
});
registrar.registerEndpoint(endpoint);
}
}
在上面的示例中,我们使用SimpleRabbitListenerEndpoint,它提供了要调用的实际MessageListener,但是您也可以构建自己的端点变体来描述自定义调用机制。
应该注意的是,您完全可以跳过@RabbitListener的使用,只通过RabbitListenerConfigurer以编程方式注册端点。
Annotated Endpoint Method Signature
到目前为止,我们已经在端点中注入了一个简单的字符串,但它实际上可以有一个非常灵活的方法签名。让我们重写它注入一个自定义头部订单:
@Component
public class MyService {
@RabbitListener(queues = "myQueue")
public void processOrder(Order order, @Header("order_type") String orderType) {
...
}
}
这些是可以注入侦听器端点的主要元素:
原始的org.springframework.amqp.core.Message。
接受com.rabbitmq.client.Channel 接收消息的通道。
org.springframework.messaging.Message 表示传入AMQP消息。注意,此消息同时包含自定义头和标准头(由AmqpHeaders定义)。
从1.6版开始,入站deliveryMode报头现在可以在名为AmqpHeaders的报头中使AmqpHeaders.RECEIVED_DELIVERY_MODE而不是AmqpHeaders.DELIVERY_MODE。
@Header-注释的方法参数来提取特定的标头值,包括标准的AMQP标头。
@Headers -带注释的参数,该参数也必须可分配给java.util.Map用于访问所有标题。
不受支持的非注释元素(即消息和通道)被认为是有效负载。您可以通过使用@Payload注释参数来显式地实现这一点。您还可以通过添加额外的@Valid来打开验证。
注入Spring消息抽象的能力对于从存储在特定于传输的消息中的所有信息中获益尤其有用,而无需依赖于特定于传输的API。
@RabbitListener(queues = "myQueue")
public void processOrder(Message<Order> order) { ...
}
方法参数的处理由DefaultMessageHandlerMethodFactory提供,可以进一步定制它来支持其他方法参数。转换和验证支持也可以在那里定制。
例如,如果我们想在处理订单之前确保订单是有效的,我们可以用@Valid标注有效负载,并配置必要的验证器如下:
@Configuration
@EnableRabbit
public class AppConfig implements RabbitListenerConfigurer {
@Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
}
@Bean
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(myValidator());
return factory;
}
}
Listening to Multiple Queues
使用queues属性时,可以指定关联容器可以侦听多个队列。您可以使用@Header注释使接收消息的队列名称对POJO方法可用:
@Component
public class MyService {
@RabbitListener(queues = { "queue1", "queue2" } )
public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
...
}
}
从1.5版开始,您可以使用属性占位符外部化队列名称,SpEL:
@Component
public class MyService {
@RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" )
public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
...
}
}
在1.5版本之前,只能以这种方式指定一个队列;每个队列都需要一个单独的属性。
Reply Management
MessageListenerAdapter中的现有支持已经允许您的方法具有非空返回类型。在这种情况下,调用的结果被封装在消息中,发送到原始消息的ReplyToAddress头中指定的地址中,或者在侦听器上配置的默认地址中。现在可以使用消息传递抽象的@SendTo注释设置默认地址。
假设我们的processOrder方法现在应该返回一个OrderStatus,那么可以将其编写为如下代码来自动发送一个回复:
@RabbitListener(destination = "myQueue")
@SendTo("status")
public OrderStatus processOrder(Order order) {
// order processing
return status;
}
如果需要以与传输无关的方式设置附加头,可以返回一条消息,比如:
@RabbitListener(destination = "myQueue")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
// order processing
return MessageBuilder
.withPayload(status)
.setHeader("code", 1234)
.build();
}
@SendTo值被假定为遵循模式exchange/routingKey的应答交换和路由键对,其中可以省略其中的一个部分。有效值为:
foo/bar
- 应答交换机和路由键。
foo/
- 应答交换机和默认的(空)路由键
bar
or /bar
- 应答路由键和默认的(空)交换机
/
or empty - 应答默认的路由键和默认的(空)交换机
@SendTo也可以不使用value属性。这种情况等于一个空的sendTo模式。@SendTo仅在入站消息没有replyToAddress属性时使用。
从1.5版本开始,@SendTo值可以是一个bean初始化SpEL表达式,例如…
@RabbitListener(queues = "test.sendTo.spel")
@SendTo("#{spelReplyTo}")
public String capitalizeWithSendToSpel(String foo) {
return foo.toUpperCase();
}
...
@Bean
public String spelReplyTo() {
return "test.sendTo.reply.spel";
}
表达式必须计算为字符串,字符串可以是简单的队列名称(发送到默认交换器),也可以是上面讨论的表单exchange/routingKey。
# {…}表达式在初始化期间计算一次。
对于动态回复路由,消息发送者应该包含reply_to消息属性,或者使用下面描述的替代运行时SpEL表达式。
从1.6版开始,@SendTo可以是一个SpEL表达式,在运行时根据请求和响应进行计算:
@RabbitListener(queues = "test.sendTo.spel")
@SendTo("!{'some.reply.queue.with.' + result.queueName}")
public Bar capitalizeWithSendToSpel(Foo foo) {
return processTheFooAndReturnABar(foo);
}
SpEL表达式的运行时属性用!{…}分隔符。表达式的计算上下文#root对象有三个属性:
request
- theo.s.amqp.core.Message
request object.source
- theo.s.messaging.Message<?>
after conversion.result
- the method result.
上下文有一个map属性访问器、一个标准类型转换器和一个bean解析器,允许引用上下文中的其他bean(例如@someBeanName.determineReplyQ(request, result))。
总之,# {…}在初始化过程中计算一次,#root对象作为应用程序上下文;bean由它们的名称引用。! {…}在运行时为具有上述属性的根对象的每条消息求值,bean的名称以@前缀引用。
Multi-Method Listeners
从1.5.0版本开始,现在可以在类级别指定@RabbitListener注释。与新的@RabbitHandler注释一起,这允许单个侦听器根据传入消息的有效负载类型调用不同的方法。这最好用一个例子来描述:
@RabbitListener(id="multi", queues = "someQueue")
public class MultiListenerBean {
@RabbitHandler
@SendTo("my.reply.queue")
public String bar(Bar bar) {
...
}
@RabbitHandler
public String baz(Baz baz) {
...
}
@RabbitHandler
public String qux(@Header("amqp_receivedRoutingKey") String rk, @Payload Qux qux) {
...
}
}
在本例中,如果转换的有效负载是Bar、Baz或Qux,则调用单个@RabbitHandler方法。重要的是要理解,系统必须能够识别基于有效负载类型的唯一方法。检查类型是否可分配给没有注释或使用@Payload注释进行注释的单个参数。注意,与上面描述的方法级别@RabbitListener中讨论的方法签名相同。
注意,必须在每个方法上指定@SendTo(如果需要);它在类级别上不受支持。
@Repeatable @RabbitListener
从1.6版开始,@RabbitListener注释被标记为@Repeatable。这意味着注释可以多次出现在同一个带注释的元素(方法或类)上。在本例中,为每个注释创建一个单独的侦听器容器,每个注释都调用相同的侦听器@Bean。可重复注释可与Java 8或以上版本一起使用;当使用Java 7或更早版本时,可以通过使用@RabbitListener“容器”注释(带有@RabbitListener注释数组)实现相同的效果。
Proxy @RabbitListener and Generics
如果您的服务打算代理(例如@Transactional),当接口具有通用参数时,需要考虑一些问题。具有通用接口和特定的实现,例如:
interface TxService<P> {
String handle(P payload, String header);
}
static class TxServiceImpl implements TxService<Foo> {
@Override
@RabbitListener(...)
public String handle(Foo foo, String rk) {
...
}
}
您必须切换到CGLIB目标类代理,因为接口句柄方法的实际实现是桥接方法。在事务管理的情况下,CGLIB的使用是使用注释选项- @EnableTransactionManagement(proxyTargetClass = true)配置的。在这种情况下,所有的注解都必须在实现的目标方法上声明:
static class TxServiceImpl implements TxService<Foo> {
@Override
@Transactional
@RabbitListener(...)
public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) {
...
}
}
Container Management
为注释创建的容器不会在应用程序上下文中注册。您可以通过调用RabbitListenerEndpointRegistry bean上的getListenerContainers()来获得所有容器的集合。然后,您可以遍历这个集合,例如,停止/启动所有容器,或者调用注册表本身上的生命周期方法,这些方法将调用每个容器上的操作。
您还可以使用它的id getListenerContainer(String id)获得对单个容器的引用;例如,上面代码片段创建的容器的register . getlistenercontainer(“multi”)。
从1.5.2版本开始,您可以使用getListenerContainerIds()获取已注册容器的id s。
从1.5版开始,现在可以在RabbitListener端点上为容器分配一个组。这提供了一种机制来获取对容器子集的引用;添加组属性会导致集合类型<MessageListenerContainer>的bean在具有组名称的上下文中注册。
Threading and Asynchronous Consumers
异步使用者涉及许多不同的线程。
在SimpleMessageListener中配置的TaskExecutor中的线程用于在RabbitMQ客户机交付新消息时调用MessageListener。如果没有配置,则使用SimpleAsyncTaskExecutor。如果使用池执行器,请确保池大小足以处理配置的并发性。
当使用默认SimpleAsyncTaskExecutor时,对于调用侦听器的线程,侦听器容器beanName用作threadNamePrefix。这对日志分析很有用;通常建议始终在日志追加器配置中包含线程名。当TaskExecutor通过SimpleMessageListenerContainer上的TaskExecutor属性被特别提供时,就按原样使用它,无需修改。建议使用类似的技术来命名由自定义TaskExecutor bean定义创建的线程,以帮助在日志消息中进行线程标识。
在CachingConnectionFactory中配置的执行器在创建连接时被传递到RabbitMQ客户机,其线程用于向侦听器容器传递新消息。在编写本文时,如果没有配置该线程池,客户机将使用池大小为5的内部线程池执行器。
RabbitMQ客户机使用ThreadFactory为低级I/O(套接字)操作创建线程。要修改这个工厂,您需要配置底层RabbitMQ ConnectionFactory,正如在“配置底层客户机连接工厂”一节中讨论的那样。
Detecting Idle Asynchronous Consumers 检测空闲异步消费者
虽然效率很高,但是异步使用者的一个问题是检测何时空闲—如果在一段时间内没有消息到达,用户可能希望采取一些行动。
从1.6版开始,现在可以配置侦听器容器,以便在没有消息传递的情况下发布ListenerContainerIdleEvent。当容器处于空闲状态时,每隔几毫秒就会发布一个事件。
要配置此功能,请在容器上设置idleEventInterval:
xml
<rabbit:listener-container connection-factory="connectionFactory"
...
idle-event-interval="60000"
...
>
<rabbit:listener id="container1" queue-names="foo" ref="myListener" method="handle" />
</rabbit:listener-container>
Java
@Bean
public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
...
container.setIdleEventInterval(60000L);
...
return container;
}
@RabbitListener
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setIdleEventInterval(60000L);
...
return factory;
}
在每种情况下,当容器空闲时,每分钟将发布一次事件。
Event Consumption 事件消费
您可以通过实现ApplicationListener来捕获这些事件——要么是普通的侦听器,要么是缩小到只接收此特定事件的侦听器。您还可以使用在Spring Framework 4.2中引入的@EventListener。
下面的示例将@RabbitListener和@EventListener合并到一个类中。重要的是要理解应用程序侦听器将获取所有容器的事件,因此如果希望根据哪个容器空闲采取特定的操作,可能需要检查侦听器id。您还可以为此使用@EventListener条件。
事件有4个属性:
source
-侦听器容器实例
id
-侦听器id(或容器bean名称)
- idleTime——事件发布时容器处于空闲状态的时间
- queueNames——容器侦听的队列的名称
public class Listener {
@RabbitListener(id="foo", queues="#{queue.name}")
public String listen(String foo) {
return foo.toUpperCase();
}
@EventListener(condition = "event.listenerId == 'foo'")
public void onApplicationEvent(ListenerContainerIdleEvent event) {
...
}
}
重要:事件监听器将看到所有容器的事件;因此,在上面的示例中,我们根据侦听器ID缩小接收的事件。
谨慎:如果您希望使用idle事件来停止lister容器,则不应该在调用侦听器的线程上调用container.stop()——这将导致延迟和不必要的日志消息。相反,应该将事件传递给另一个线程,该线程可以停止容器。
3.1.7 Message Converters
Introduction
AmqpTemplate还定义了几种发送和接收消息的方法,这些方法将委托给MessageConverter。MessageConverter本身非常简单。它为每个方向提供了一个方法:一个用于转换为消息,另一个用于为从消息转换。注意,在转换为消息时,您还可以提供对象之外的属性。“object”参数通常对应于消息体。
public interface MessageConverter {
Message toMessage(Object object, MessageProperties messageProperties)
throws MessageConversionException;
Object fromMessage(Message message) throws MessageConversionException;
}
下面列出了AmqpTemplate上的相关消息发送方法。它们比我们前面讨论的方法更简单,因为它们不需要Message实例。相反,MessageConverter负责“创建”每个消息,方法是将提供的对象转换为消息体的字节数组,然后添加任何提供的MessageProperties。
void convertAndSend(Object message) throws AmqpException;
void convertAndSend(String routingKey, Object message) throws AmqpException;
void convertAndSend(String exchange, String routingKey, Object message)
throws AmqpException;
void convertAndSend(Object message, MessagePostProcessor messagePostProcessor)
throws AmqpException;
void convertAndSend(String routingKey, Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException;
void convertAndSend(String exchange, String routingKey, Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException;
在接收端,只有两种方法:一种接受队列名称,另一种依赖于已设置的模板的“queue”属性。
Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
在“异步消费者”一节中提到的MessageListenerAdapter也使用MessageConverter。
SimpleMessageConverter
MessageConverter策略的默认实现称为SimpleMessageConverter。如果您没有显式地配置替代方案,则RabbitTemplate的实例将使用此转换器。它处理基于文本的内容、序列化的Java对象和简单字节数组。
Converting From a Message
如果输入消息的内容类型以“text”开头(例如:,它还将检查content-encoding属性,以确定将消息体字节数组转换为Java字符串时使用的字符集。如果没有在输入消息上设置内容编码属性,则默认使用“UTF-8”字符集。如果需要覆盖默认设置,可以配置SimpleMessageConverter的一个实例,设置它的“defaultCharset”属性,然后将其注入RabbitTemplate实例。
如果将输入消息的content-type属性值设置为“application/x- Java -serialize -object”,SimpleMessageConverter将尝试将字节数组反序列化(重新水合物化)到Java对象中。虽然这对于简单的原型开发可能很有用,但是通常不建议依赖Java序列化,因为它会导致生产者和消费者之间的紧密耦合。当然,它还排除了任何一方使用非java系统的可能性。由于AMQP是一种线级协议,因此很不幸,由于这些限制而失去了这种优势。在接下来的两部分中,我们将探索一些替代方法,在不依赖Java序列化的情况下传递丰富的域对象内容。
对于所有其他内容类型,SimpleMessageConverter将以字节数组的形式直接返回消息体内容。
有关重要信息,请参阅“Java反序列化”一节。
Converting To a Message
当从任意Java对象转换为消息时,SimpleMessageConverter同样处理字节数组、字符串和可序列化实例。它将把这些转换为字节(对于字节数组,没有什么要转换的),并相应地设置content-type属性。如果要转换的对象不匹配其中一种类型,则消息体将为null。
SerializerMessageConverter
这个转换器类似于SimpleMessageConverter,但它可以配置其他Spring框架序列化器和反序列化器实现,用于应用程序/x-java序列化对象转换。
有关重要信息,请参阅“Java反序列化”一节。
Jackson2JsonMessageConverter
Converting to a Message
如前一节所述,一般不建议依赖Java序列化。一个比较常见的替代方法是JSON (JavaScript对象表示法),它在不同的语言和平台上更加灵活和可移植。可以在任何RabbitTemplate实例上配置转换器,以覆盖它对SimpleMessageConverter缺省值的使用。Jackson2JsonMessageConverter使用com.fasterxml.jackson
2.x库。
<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
<property name="connectionFactory" ref="rabbitConnectionFactory"/>
<property name="messageConverter">
<bean class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter">
<!-- if necessary, override the DefaultClassMapper -->
<property name="classMapper" ref="customClassMapper"/>
</bean>
</property>
</bean>
如上所示,Jackson2JsonMessageConverter默认使用DefaultClassMapper。类型信息被添加到MessageProperties中(并从中检索)。如果入站消息在MessageProperties中不包含类型信息,但您知道预期的类型,则可以使用defaultType属性配置静态类型。
<bean id="jsonConverterWithDefaultType"
class="o.s.amqp.support.converter.Jackson2JsonMessageConverter">
<property name="classMapper">
<bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
<property name="defaultType" value="foo.PurchaseOrder"/>
</bean>
</property>
</bean>
Converting from a Message
入站消息根据发送系统添加到头中的类型信息转换为对象。
在1.6之前的版本中,如果没有类型信息,转换将失败。从1.6版开始,如果缺少类型信息,转换器将使用Jackson默认值(通常是map)转换JSON。
此外,从1.6版开始,当使用@RabbitListener注释(关于方法)时,推断出的类型信息被添加到MessageProperties;这允许转换器转换为目标方法的参数类型。这只适用于没有注释的一个参数或@Payload注释的单个参数。在分析过程中忽略消息类型的参数。
重要:默认情况下,推断的类型信息将覆盖入站的_typeid__和由发送系统创建的相关头。这允许接收系统自动转换为不同的域对象。这只适用于参数类型是具体的(而不是抽象的或接口)或来自java.util包的情况。在所有其他情况下,将使用_typeid__和相关的头文件。在某些情况下,您可能希望覆盖默认行为并始终使用_typeid__信息。例如,假设您有一个@RabbitListener,它接受Foo参数,但是消息包含一个Bar,它是Foo的子类(它是具体的)。推断的类型将不正确。要处理这种情况,将Jackson2JsonMessageConverter上的TypePrecedence 属性设置为TYPE_ID,而不是推断的默认值。该属性实际上位于转换器的DefaultJackson2JavaTypeMapper上,但是为了方便起见,转换器上提供了setter。如果注入自定义类型映射器,则应在映射器上设置属性。
从消息转换时,传入的MessageProperties.getContentType()必须符合json(使用逻辑contentType.contains(“json”))。否则,警告日志消息无法转换具有内容类型[…getbody()以字节[]的形式返回。因此,为了满足客户端对Jackson2JsonMessageConverter的要求,生产者必须添加contentType message属性,例如:as application/json、text/x-json或简单地使用Jackson2JsonMessageConverter,它将自动设置头部。
@RabbitListener
public void foo(Foo foo) {...}
@RabbitListener
public void foo(@Payload Foo foo, @Header("amqp_consumerQueue") String queue) {...}
@RabbitListener
public void foo(Foo foo, o.s.amqp.core.Message message) {...}
@RabbitListener
public void foo(Foo foo, o.s.messaging.Message<Foo> message) {...}
@RabbitListener
public void foo(Foo foo, String bar) {...}
@RabbitListener
public void foo(Foo foo, o.s.messaging.Message<?> message) {...}
在上面的前四种情况中,转换器将尝试转换为Foo类型。第五个示例是无效的,因为我们无法确定哪个参数应该接收消息有效负载。对于第6个示例,由于泛型类型是通配符类型,所以将应用Jackson缺省值。
但是,您可以创建一个自定义转换器,并使用targetMethod message属性来决定将JSON转换为哪种类型。只有在方法级别声明@RabbitListener注释时才能实现这种类型推断。使用类级别@RabbitListener,转换后的类型用于选择要调用哪个@RabbitHandler方法。因此,基础设施提供targetObject消息属性,自定义转换器可以使用该属性来确定类型。
重要:从1.6.11版本开始,Jackson2JsonMessageConverter和DefaultJackson2JavaTypeMapper (DefaultClassMapper)提供trustedPackages选项来克服序列化gadget的漏洞。默认情况下,为了向后兼容,Jackson2JsonMessageConverter信任所有包—使用*作为该选项。
MarshallingMessageConverter
略。。。
ContentTypeDelegatingMessageConverter
这个类是在1.4.2版本中引入的,允许基于MessageProperties中的content type属性将委托给特定的MessageConverter。默认情况下,如果没有contentType属性或值与任何配置的转换器都不匹配,它将委托给SimpleMessageConverter。
<bean id="contentTypeConverter" class="ContentTypeDelegatingMessageConverter">
<property name="delegates">
<map>
<entry key="application/json" value-ref="jsonMessageConverter" />
<entry key="application/xml" value-ref="xmlMessageConverter" />
</map>
</property>
</bean>
Java Deserialization
略。。。
Message Properties Converters
MessagePropertiesConverter策略接口用于在Rabbit客户机BasicProperties 和Spring AMQP MessageProperties之间进行转换。默认实现(DefaultMessagePropertiesConverter)通常足以满足大多数目的,但如果需要,您可以实现自己的实现。当长度不大于1024字节时,默认的属性转换器将LongString类型的BasicProperties元素转换为String s。较大的长字符串s不进行转换(参见下面)。可以使用构造函数参数重写此限制。
从1.6版开始,DefaultMessagePropertiesConverter将比长字符串限制(默认值为1024)更长的头保留为长字符串s。您可以通过getBytes[]、toString()或getStream()方法访问内容。
之前,DefaultMessagePropertiesConverter将这些头“转换”为DataInputStream(实际上它只是引用了LongString的DataInputStream)。在输出时,这个头没有被转换(除了转换成字符串,例如java.io)。通过调用流上的toString(), DataInputStream@1d057a39。
输入的大长字符串头现在在输出上也正确地“转换”了(默认情况下)。
提供了一个新的构造函数,使您可以像以前一样配置转换器:
/**
* Construct an instance where LongStrings will be returned
* unconverted or as a java.io.DataInputStream when longer than this limit.
* Use this constructor with 'true' to restore pre-1.6 behavior.
* @param longStringLimit the limit.
* @param convertLongLongStrings LongString when false,
* DataInputStream when true.
* @since 1.6
*/
public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... }
略。。。
3.1.8 Modifying Messages - Compression and More
存在许多扩展点,您可以在这些扩展点上对消息执行一些处理,无论是在消息发送到RabbitMQ之前,还是在消息接收之后。正如在3.1.7节“消息转换器”中所看到的,AmqpTemplate convertAndReceive操作中就有一个这样的扩展点,您可以在这里提供MessagePostProcessor。例如,在转换POJO之后,MessagePostProcessor允许您设置消息的自定义头或属性。
从1.4.2版本开始,已经向RabbitTemplate添加了额外的扩展点——setBeforePublishPostProcessors()和setAfterReceivePostProcessors()。第一种方法使后处理器能够在发送到RabbitMQ之前立即运行。当使用批处理(请参阅“批处理”一节)时,这将在批处理组装之后和批处理发送之前调用。第二个函数在接收到消息后立即调用。
这些扩展点用于压缩等功能,为此,提供了几个MessagePostProcessor:
用于在发送前压缩消息:
- GZipPostProcessor
- ZipPostProcessor
用于解压缩接收到的消息:
- GUnzipPostProcessor
- UnzipPostProcessor
类似地,SimpleMessageListenerContainer也有一个setAfterReceivePostProcessors()方法,允许在容器接收到消息后执行解压缩。
3.1.9 Request/Reply Messaging
Introduction
AmqpTemplate还提供了各种sendAndReceive方法,它们接受与您在上面看到的单向发送操作(exchange、routingKey和Message)相同的参数选项。这些方法对于请求/应答场景非常有用,因为它们在发送前处理必要的“reply-to”属性的配置,并且可以在内部为此目的创建的独占队列上侦听应答消息。
当MessageConverter同时应用于请求和应答时,也可以使用类似的请求/应答方法。这些方法被命名为convertSendAndReceive。有关详细信息,请参阅AmqpTemplate的Javadoc。
从1.5.0版本开始,每个sendAndReceive方法变体都有一个重载版本,该版本接受CorrelationData。与正确配置的连接工厂一起,这允许接收发布者确认操作的发送端。有关更多信息,请参见“发布者确认和返回”一节。
Reply Timeout
默认情况下,发送和接收方法将在5秒后超时并返回null。这可以通过设置replyTimeout属性来修改。从1.5版开始,如果将mandatory属性设置为true(或者对特定消息的强制表达式计算为true),如果消息不能传递到队列,则会抛出AmqpMessageReturnedException 。这个异常返回了消息、replyCode、replyText属性以及用于发送的exchange和routingKey。
该特性使用publisher返回,并通过在CachingConnectionFactory上将publisherReturns设置为true来启用(请参阅“publisher确认和返回”一节)。另外,您必须没有使用RabbitTemplate注册自己的ReturnCallback。
RabbitMQ Direct reply-to
重要:从3.4.0版本开始,RabbitMQ服务器现在支持直接回复;这消除了固定应答队列的主要原因(为了避免为每个请求创建临时队列)。从Spring AMQP version 1.4.1开始,默认情况下将使用Direct reply-to(如果服务器支持),而不是创建临时应答队列。当没有提供replyQueue(或者使用名称amq.rabbitmq.reply-to设置它)时,RabbitTemplate将自动检测是否支持直接回复,并使用它或退回到使用临时回复队列。在使用直接应答时,不需要也不应该配置应答侦听器。
应答侦听器仍然受命名队列(amq.rabbitmq.reply-to除外)的支持,允许控制应答并发性等。
从1.6版开始,如果出于某种原因,希望为每个回复使用临时的、独占的、自动删除的队列,请将useTemporaryReplyQueues属性设置为true。如果您设置了replyAddress,则会忽略此属性。
通过子类化RabbitTemplate并覆盖useDirectReplyTo(),可以将是否使用direct reply-to的决策更改为使用不同的标准。方法只调用一次;当发送第一个请求时。
Message Correlation With A Reply Queue
当使用固定的应答队列(amq.rabbitmq.reply-to除外)时,需要提供相关数据,以便应答可以与请求关联。参见RabbitMQ远程过程调用(RPC)。默认情况下,标准的correlationId属性将用于保存相关数据。但是,如果希望使用自定义属性来保存相关数据,可以在<rabbit-template/>上设置correlation-key属性。显式地将属性设置为correlationId与省略属性相同。当然,客户端和服务器必须对相关数据使用相同的头。
Spring AMQP 1.1版本对该数据使用了自定义属性spring_reply_correlation。如果您希望使用当前版本恢复到这种行为,可能是为了维护与使用1.1的其他应用程序的兼容性,您必须将属性设置为spring_reply_correlation。
Reply Listener Container
当使用3.4.0之前的RabbitMQ版本时,每个回复都使用一个新的临时队列。但是,可以在模板上配置单个应答队列,这将更加有效,还允许您在该队列上设置参数。
但是,在本例中,还必须提供一个<reply-listener/>子元素。此元素为应答队列提供侦听器容器,模板是侦听器。除了从模板配置中继承的connection-factory和Message -converter之外,元素上允许< Listener - Container />上允许的所有第3.1.15节“消息侦听器容器配置”属性。
重要:如果你运行应用程序的多个实例,或者使用多个RabbitTemplate年代,您必须使用一个独特的应答队列为每个——RabbitMQ没有能力来选择从一个队列的消息,如果他们都使用相同的队列,每个实例将争夺,不一定收到自己的回答。
<rabbit:template id="amqpTemplate"
connection-factory="connectionFactory"
reply-queue="replies"
reply-address="replyEx/routeReply">
<rabbit:reply-listener/>
</rabbit:template>
虽然容器和模板共享一个连接工厂,但它们不共享通道,因此请求和响应不会在同一个事务中执行(如果是事务性的)。
在1.5.0版本之前,回复地址属性不可用,回复总是使用默认的exchange和作为路由键的回复队列名称进行路由。这仍然是默认值,但是现在可以指定新的reply-address属性。回复地址可以包含一个格式为<exchange>/<routingKey>的地址,应答将路由到指定的交换,并路由到使用路由键绑定的队列。复制地址优先于复制队列。必须将<reply-listener>配置为单独的<listener-container>组件,当只使用reply-address时,无论如何,reply-address和reply-queue(或<listener-container>上的queues属性)必须在逻辑上引用相同的队列。
通过这种配置,SimpleListenerContainer用于接收响应;使用RabbitTemplate作为MessageListener。当使用<rabbit:template/> namespace元素定义模板时,如上所示,解析器将容器定义为侦听器,并将模板中的连接作为侦听器。
略。。。
AsyncRabbitTemplate
版本1.6引入了AsyncRabbitTemplate。这与AmqpTemplate上的sendAndReceive(和convertSendAndReceive)方法类似,但是它们没有阻塞,而是返回ListenableFuture。
sendAndReceive方法返回一个RabbitMessageFuture;convertSendAndReceive方法返回一个RabbitConverterFuture。
您可以稍后通过调用get()同步检索结果,也可以注册一个回调函数,该回调函数将与结果异步调用。
@Autowired
private AsyncRabbitTemplate template;
...
public void doSomeWorkAndGetResultLater() {
...
ListenableFuture<String> future = this.template.convertSendAndReceive("foo");
// do some more work
String reply = null;
try {
reply = future.get();
}
catch (ExecutionException e) {
...
}
...
}
public void doSomeWorkAndGetResultAsync() {
...
RabbitConverterFuture<String> future = this.template.convertSendAndReceive("foo");
future.addCallback(new ListenableFutureCallback<String>() {
@Override
public void onSuccess(String result) {
...
}
@Override
public void onFailure(Throwable ex) {
...
}
});
...
}
如果设置了mandatory,并且不能传递消息,那么将来会抛出ExecutionException,原因是AmqpMessageReturnedException 封装了返回的消息和关于返回的信息。
如果enableConfirms 被设置,future将具有一个属性confirm,该属性本身是ListenableFuture<Boolean>, true表示发布成功。如果确认将来是假的,RabbitFuture将有一个进一步的属性nackCause——失败的原因,如果可用的话。
重要:如果在应答之后收到发布者confirm,则将丢弃它——因为应答意味着发布成功。
将模板上的receiveTimeout属性设置为超时响应(默认值为30000 - 30秒)。如果发生超时,the future使用AmqpReplyTimeoutException完成。
该模板实现了SmartLifecycle;在有待处理的答复时停止模板将导致取消待处理的Future
。
Spring Remoting with AMQP
略。。。
3.1.10 Configuring the broker
Introduction
AMQP规范描述了如何使用协议在代理上配置队列、交换器和绑定。这些操作可以从0.8或更高版本移植到org.springframework.amqp.core包中的AmqpAdmin接口中。该类的RabbitMQ实现是RabbitAdmin,位于org.springframework.amqp.rabbit.core包。
AmqpAdmin接口基于Spring AMQP域抽象,如下图所示:
public interface AmqpAdmin {
// Exchange Operations
void declareExchange(Exchange exchange);
void deleteExchange(String exchangeName);
// Queue Operations
Queue declareQueue();
String declareQueue(Queue queue);
void deleteQueue(String queueName);
void deleteQueue(String queueName, boolean unused, boolean empty);
void purgeQueue(String queueName, boolean noWait);
// Binding Operations
void declareBinding(Binding binding);
void removeBinding(Binding binding);
Properties getQueueProperties(String queueName);
}
getQueueProperties()方法返回关于队列的一些有限信息(消息计数和消费者计数)。返回属性的键可以作为常量在RabbitTemplate中使用(QUEUE_NAME、QUEUE_MESSAGE_COUNT、QUEUE_CONSUMER_COUNT)。RabbitMQ REST API在QueueInfo对象中提供了更多的信息。
无参数的declareQueue()方法的作用是:在代理上定义一个队列,该队列的名称是自动生成的。这个自动生成的队列的附加属性是exclusive=true、autoDelete=true和persistent =false。
declareQueue(Queue queue)方法接受队列对象并返回声明的队列的名称。如果提供的队列的name属性是空字符串,则代理使用生成的名称声明队列,并将该名称返回给调用者。队列对象本身没有更改。此功能只能通过直接调用RabbitAdmin以编程方式使用。管理员不支持通过在应用程序上下文中声明式定义队列来实现自动声明。
这与AnonymousQueue不同,在AnonymousQueue中,框架生成一个惟一的(UUID)名称,并将持久性设置为false和exclusive,将autoDelete设置为true。如果<rabbit:queue/>的name属性为空或缺失,则始终会创建一个AnonymousQueue。
请参阅名为“AnonymousQueue”的部分,以了解为什么宁愿使用AnonymousQueue而不是由经纪公司生成的队列名称,以及如何控制名称的格式。声明性队列必须有固定的名称,因为它们可能在上下文的其他地方引用,例如在侦听器中:
<rabbit:listener-container>
<rabbit:listener ref="listener" queue-names="#{someQueue.name}" />
</rabbit:listener-container>
See the section called “Automatic Declaration of Exchanges, Queues and Bindings”.
这个接口的RabbitMQ实现是RabbitAdmin,当使用Spring XML配置时,它看起来像这样:
<rabbit:connection-factory id="connectionFactory"/>
<rabbit:admin id="amqpAdmin" connection-factory="connectionFactory"/>
当CachingConnectionFactory缓存模式为CHANNEL(缺省模式)时,RabbitAdmin实现自动延迟声明队列、交换和绑定,这些声明在相同的ApplicationContext中声明。当打开到代理的连接时,这些组件将被声明为s0on。有一些命名空间的功能,使这非常方便,例如在股票样本应用程序,我们有:
<rabbit:queue id="tradeQueue"/>
<rabbit:queue id="marketDataQueue"/>
<fanout-exchange name="broadcast.responses"
xmlns="http://www.springframework.org/schema/rabbit">
<bindings>
<binding queue="tradeQueue"/>
</bindings>
</fanout-exchange>
<topic-exchange name="app.stock.marketdata"
xmlns="http://www.springframework.org/schema/rabbit">
<bindings>
<binding queue="marketDataQueue" pattern="${stocks.quote.pattern}"/>
</bindings>
</topic-exchange>
在上面的例子中,我们使用匿名队列(实际上在内部只是使用框架生成的名称的队列,而不是代理生成的名称),并通过ID引用它们。如。
<rabbit:queue name="stocks.trade.queue"/>
您可以同时提供id和name属性。这允许您通过独立于队列名称的id引用队列(例如在绑定中)。它还允许标准Spring特性,如属性占位符和队列名称的SpEL表达式;当使用名称作为bean标识符时,这些特性不可用。
队列可以配置额外的参数,例如x-message-ttl或x-ha-policy。使用名称空间支持,它们以参数名称/参数值对映射的形式提供,使用<rabbit:queue-arguments>元素。
<rabbit:queue name="withArguments">
<rabbit:queue-arguments>
<entry key="x-ha-policy" value="all"/>
</rabbit:queue-arguments>
</rabbit:queue>
默认情况下,参数被假定为字符串。对于其他类型的参数,需要提供该类型。
<rabbit:queue name="withArguments">
<rabbit:queue-arguments value-type="java.lang.Long">
<entry key="x-message-ttl" value="100"/>
</rabbit:queue-arguments>
</rabbit:queue>
当提供混合类型的参数时,为每个条目元素提供类型:
<rabbit:queue name="withArguments">
<rabbit:queue-arguments>
<entry key="x-message-ttl">
<value type="java.lang.Long">100</value>
</entry>
<entry key="x-ha-policy" value="all"/>
</rabbit:queue-arguments>
</rabbit:queue>
在Spring Framework 3.2及更高版本中,可以更简洁地声明这一点:
<rabbit:queue name="withArguments">
<rabbit:queue-arguments>
<entry key="x-message-ttl" value="100" value-type="java.lang.Long"/>
<entry key="x-ha-policy" value="all"/>
</rabbit:queue-arguments>
</rabbit:queue>
重要:RabbitMQ代理不允许声明参数不匹配的队列。例如,如果队列已经存在,没有时间使用参数,并且您试图使用key="x-message-ttl" value="100"来声明它,则会抛出异常。
默认情况下,当任何异常发生时,RabbitAdmin将立即停止处理所有声明;这可能会导致下游问题——例如侦听器容器初始化失败,因为没有声明另一个队列(在错误队列之后定义)。
可以通过在RabbitAdmin上将ignore-declaration-exceptions属性设置为true来修改此行为。这个选项指示RabbitAdmin记录异常,并继续声明其他元素。当使用java配置RabbitAdmin时,这个属性是ignoreDeclarationExceptions。这是一个适用于所有元素的全局设置,队列、交换器和绑定都有一个类似的属性,它只适用于这些元素。
在1.6版之前,此属性仅在通道上发生IOException时才生效,比如当前属性和所需属性之间不匹配时。现在,这个属性对任何异常都有效,包括TimeoutException等。
此外,任何声明异常都会导致一个DeclarationExceptionEvent的发布,该事件是一个ApplicationEvent,可以由上下文中的任何ApplicationListener使用。该事件包含对admin的引用、正在声明的元素和Throwable。
从1.3版开始,HeadersExchange可以配置为匹配多个头文件;您还可以指定是否必须匹配任何或所有标头:
<rabbit:headers-exchange name="headers-test">
<rabbit:bindings>
<rabbit:binding queue="bucket">
<rabbit:binding-arguments>
<entry key="foo" value="bar"/>
<entry key="baz" value="qux"/>
<entry key="x-match" value="all"/>
</rabbit:binding-arguments>
</rabbit:binding>
</rabbit:bindings>
</rabbit:headers-exchange>
从version 1.6开始,可以使用internal
标志配置交换器(默认为false),这样的交换器将通过RabbitAdmin在代理上正确配置(如果在应用程序上下文中存在)。如果内部标志对交换为真,RabbitMQ将不允许客户端使用交换。这对于死信交换或exchange-to-exchange绑定非常有用,因为您不希望该交换被发布方直接使用。
要查看如何使用Java配置AMQP基础设施,请查看Stock 样例应用程序,其中有@Configuration类AbstractStockRabbitConfiguration,它又具有RabbitClientConfiguration和RabbitServerConfiguration子类。AbstractStockRabbitConfiguration的代码如下所示
@Configuration
public abstract class AbstractStockAppRabbitConfiguration {
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory =
new CachingConnectionFactory("localhost");
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
return connectionFactory;
}
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setMessageConverter(jsonMessageConverter());
configureRabbitTemplate(template);
return template;
}
@Bean
public MessageConverter jsonMessageConverter() {
return new JsonMessageConverter();
}
@Bean
public TopicExchange marketDataExchange() {
return new TopicExchange("app.stock.marketdata");
}
// additional code omitted for brevity
}
在Stock应用程序中,服务器使用以下@Configuration类配置:
@Configuration
public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration {
@Bean
public Queue stockRequestQueue() {
return new Queue("app.stock.request");
}
}
这是@Configuration类的整个继承链的末尾。最终的结果是,TopicExchange和队列将在应用程序启动时声明给代理。在服务器配置中,TopicExchange不像在客户机应用程序中那样绑定到队列。然而,股票请求队列被自动绑定到AMQP默认交易所—此行为由规范定义。
client @Configuration类更有趣,如下所示。
@Configuration
public class RabbitClientConfiguration extends AbstractStockAppRabbitConfiguration {
@Value("${stocks.quote.pattern}")
private String marketDataRoutingKey;
@Bean
public Queue marketDataQueue() {
return amqpAdmin().declareQueue();
}
/**
* Binds to the market data exchange.
* Interested in any stock quotes
* that match its routing key.
*/
@Bean
public Binding marketDataBinding() {
return BindingBuilder.bind(
marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey);
}
// additional code omitted for brevity
}
客户端通过AmqpAdmin上的declareQueue()方法声明另一个队列,并使用在属性文件中外部化的路由模式将该队列绑定到市场数据交换。
Builder API for Queues and Exchanges
Version 1.6引入了一个方便的fluent API,用于在使用Java配置时配置队列和交换对象:
@Bean
public Queue queue() {
return QueueBuilder.nonDurable("foo")
.autoDelete()
.exclusive()
.withArgument("foo", "bar")
.build();
}
@Bean
public Exchange exchange() {
return ExchangeBuilder.directExchange("foo")
.autoDelete()
.internal()
.withArgument("foo", "bar")
.build();
}
See the javadocs for org.springframework.amqp.core.QueueBuilder
and org.springframework.amqp.core.ExchangeBuilder
for more information.
Declaring Collections of Exchanges, Queues, Bindings
从1.5版开始,现在可以通过一个@Bean声明多个实体,方法是重新生成一个集合。
只考虑第一个元素是可声明的集合,并且只处理此类集合中的可声明元素。
@Configuration
public static class Config {
@Bean
public ConnectionFactory cf() {
return new CachingConnectionFactory("localhost");
}
@Bean
public RabbitAdmin admin(ConnectionFactory cf) {
return new RabbitAdmin(cf);
}
@Bean
public DirectExchange e1() {
return new DirectExchange("e1", false, true);
}
@Bean
public Queue q1() {
return new Queue("q1", false, false, true);
}
@Bean
public Binding b1() {
return BindingBuilder.bind(q1()).to(e1()).with("k1");
}
@Bean
public List<Exchange> es() {
return Arrays.<Exchange>asList(
new DirectExchange("e2", false, true),
new DirectExchange("e3", false, true)
);
}
@Bean
public List<Queue> qs() {
return Arrays.asList(
new Queue("q2", false, false, true),
new Queue("q3", false, false, true)
);
}
@Bean
public List<Binding> bs() {
return Arrays.asList(
new Binding("q2", DestinationType.QUEUE, "e2", "k2", null),
new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)
);
}
@Bean
public List<Declarable> ds() {
return Arrays.<Declarable>asList(
new DirectExchange("e4", false, true),
new Queue("q4", false, false, true),
new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)
);
}
}
重要:在某些情况下,这个特性可能会导致不希望出现的副作用,因为管理员必须遍历所有Collection<?> bean。从1.7.7和2.0.4版本开始,可以通过将admin属性declareCollections设置为false禁用此功能。
Conditional Declaration
默认情况下,所有队列、交换和绑定都由应用程序上下文中的所有RabbitAdmin实例(具有auto-startup="true")声明。
从1.2版本开始,可以有条件地声明这些元素。当应用程序连接到多个代理并需要指定应该使用哪个代理声明特定的元素时,这一点特别有用。
表示这些元素的类实现了Declarable ,它有两个方法:shouldDeclare()和getDeclaringAdmins()。RabbitAdmin使用这些方法来确定一个特定的实例是否应该处理其连接上的声明。
属性作为名称空间中的属性可用,如下面的示例所示。
<rabbit:admin id="admin1" connection-factory="CF1" />
<rabbit:admin id="admin2" connection-factory="CF2" />
<rabbit:queue id="declaredByBothAdminsImplicitly" />
<rabbit:queue id="declaredByBothAdmins" declared-by="admin1, admin2" />
<rabbit:queue id="declaredByAdmin1Only" declared-by="admin1" />
<rabbit:queue id="notDeclaredByAny" auto-declare="false" />
<rabbit:direct-exchange name="direct" declared-by="admin1, admin2">
<rabbit:bindings>
<rabbit:binding key="foo" queue="bar"/>
</rabbit:bindings>
</rabbit:direct-exchange>
自动声明属性默认为true,如果未提供(或为空)已声明的对象,则所有RabbitAdmin都将声明该对象(只要管理员的自动启动属性为true;默认)。
类似地,您可以使用基于java的@Configuration来实现相同的效果。在本例中,组件将由admin1声明,而不是admin2:
@Bean
public RabbitAdmin admin() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(cf1());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
@Bean
public RabbitAdmin admin2() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(cf2());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
@Bean
public Queue queue() {
Queue queue = new Queue("foo");
queue.setAdminsThatShouldDeclare(admin());
return queue;
}
@Bean
public Exchange exchange() {
DirectExchange exchange = new DirectExchange("bar");
exchange.setAdminsThatShouldDeclare(admin());
return exchange;
}
@Bean
public Binding binding() {
Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null);
binding.setAdminsThatShouldDeclare(admin());
return binding;
}
AnonymousQueue
通常,当需要一个惟一命名的、排他的、自动删除队列时,建议使用AnonymousQueue而不是经纪器定义的队列名称(使用“”作为队列名称将导致代理生成队列名称)。
原因:
- 当建立到代理的连接时,实际上声明队列;这是在创建bean并将它们连接在一起之后很久的事情;使用队列的bean需要知道它的名称。事实上,当应用程序启动时,代理甚至可能没有运行。
- 如果与代理的连接由于某种原因丢失,管理员将使用相同的名称重新声明AnonymousQueue。如果使用代理声明的队列,队列名称将会更改。
从1.5.3版本开始,您可以控制AnonymousQueue s使用的队列名称的格式。
默认情况下,队列名称是UUID的字符串表示;例如:07afcfe9-fe77-4983-8645-0061ec61a47a。
@Bean
public Queue anon1() {
return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy());
}
@Bean
public Queue anon2() {
return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("foo-"));
}
<rabbit:queue id="uuidAnon" />
<rabbit:queue id="springAnon" naming-strategy="springNamer" />
<rabbit:queue id="customAnon" naming-strategy="customNamer" />
<bean id="springNamer" class="org.springframework.amqp.core.AnonymousQueue.Base64UrlNamingStrategy" />
<bean id="customNamer" class="org.springframework.amqp.core.AnonymousQueue.Base64UrlNamingStrategy">
<constructor-arg value="custom.gen-" />
</bean>
3.1.11 Delayed Message Exchange 延迟消息交换
该插件目前被标记为实验性的,但已经可用了一年多(在撰写本文时)。如果需要对插件进行更改,我们将尽快添加对此类更改的支持。因此,Spring AMQP中的这种支持也应该被认为是实验性的。这个功能是用RabbitMQ 3.6.0和0.0.1版插件测试的。
要使用RabbitAdmin将一个exchange声明为delayed,只需将exchange bean上的delayed属性设置为true。RabbitAdmin将使用exchange类型(Direct、Fanout等)设置x-delayed-type参数,并使用x-delayed-message类型声明交换。
使用XML配置exchange bean时,延迟属性(默认为false)也是可用的。
<rabbit:topic-exchange name="topic" delayed="true" />
要发送延迟消息,只需通过MessageProperties设置x-delay头:
MessageProperties properties = new MessageProperties();
properties.setDelay(15000);
template.send(exchange, routingKey,
MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build());
或者
rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(15000);
return message;
}
});
要检查消息是否被延迟,请在MessageProperties上使用getReceivedDelay()方法。它是一个单独的属性,以避免意外地传播到由输入消息生成的输出消息。
3.1.12 RabbitMQ REST API
启用管理插件后,RabbitMQ服务器将公开一个REST API来监视和配置代理。现在提供了用于API的Java绑定。通常,您可以直接使用该API,但是提供了一个方便的包装器来使用熟悉的Spring AMQP队列、交换和与该API绑定域对象。当使用com.rabbitmq.http.client时,可以为这些对象提供更多的信息。客户端API直接(分别为QueueInfo、ExchangeInfo和BindingInfo)。在RabbitManagementTemplate上可以执行以下操作:
public interface AmqpManagementOperations { void addExchange(Exchange exchange); void addExchange(String vhost, Exchange exchange); void purgeQueue(Queue queue); void purgeQueue(String vhost, Queue queue); void deleteQueue(Queue queue); void deleteQueue(String vhost, Queue queue); Queue getQueue(String name); Queue getQueue(String vhost, String name); List<Queue> getQueues(); List<Queue> getQueues(String vhost); void addQueue(Queue queue); void addQueue(String vhost, Queue queue); void deleteExchange(Exchange exchange); void deleteExchange(String vhost, Exchange exchange); Exchange getExchange(String name); Exchange getExchange(String vhost, String name); List<Exchange> getExchanges(); List<Exchange> getExchanges(String vhost); List<Binding> getBindings(); List<Binding> getBindings(String vhost); List<Binding> getBindingsForExchange(String vhost, String exchange); }
3.1.13 Exception Handling
使用RabbitMQ Java客户机的许多操作都可能抛出已检查的异常。例如,在很多情况下可能会抛出IOExceptions 。RabbitTemplate、SimpleMessageListenerContainer和其他Spring AMQP组件将捕获这些异常,并将其转换为运行时层次结构中的异常之一。它们在org.springframework.amqp中定义,而AmqpException是层次结构的基础。
当侦听器抛出异常时,它被包装在ListenerExecutionFailedException中,并且消息通常被代理拒绝并重新请求。将defaultRequeueRejected 设置为false将导致丢弃消息(或路由到死信交换)。正如在“消息侦听器和异步情况”一节中讨论的,侦听器可以抛出AmqpRejectAndDontRequeueException来有条件地控制这种行为。
但是,有一类错误是侦听器无法控制行为的。当遇到无法转换的消息(例如无效的content_encoding头)时,会在消息到达用户代码之前抛出一些异常。将defaultRequeueRejected 设置为true(默认值),这样的消息将被反复地重新传递。在1.3.2版本之前,用户需要编写一个自定义的ErrorHandler,如3.1.13节“异常处理”中讨论的那样,以避免这种情况。
从1.3.2版本开始,默认的ErrorHandler现在是一个ConditionalRejectingErrorHandler,它将拒绝(而不是请求)失败的消息,并带有不可恢复的错误:
o.s.amqp...MessageConversionException
o.s.messaging...MessageConversionException
o.s.messaging...MethodArgumentNotValidException
o.s.messaging...MethodArgumentTypeMismatchException
java.lang.NoSuchMethodException
java.lang.ClassCastException
当使用MessageConverter转换传入消息有效负载时,可以抛出第一个。如果在映射到@RabbitListener方法时需要额外的转换,则转换服务可能会抛出第二个。如果在侦听器中使用验证(例如@Valid)而验证失败,则可能引发第三个。如果入站消息转换为目标方法不正确的类型,则可能引发第四个。例如,该参数声明为消息<Foo>,但是接收到消息<Bar>。
第五个和第六个是在1.6.3版中添加的。
这个错误处理程序的一个实例可以配置一个FatalExceptionStrategy,这样用户就可以为条件消息拒绝提供他们自己的规则,例如,从Spring Retry(称为“消息监听器和异步情况”的部分)到BinaryExceptionClassifier的委托实现。此外,ListenerExecutionFailedException现在有一个failedMessage属性,可以在决策中使用。如果FatalExceptionStrategy.isFatal()方法返回true,错误处理程序将抛出AmqpRejectAndDontRequeueException。当一个异常被确定为致命时,默认的FatalExceptionStrategy会记录一条警告消息。
由于版本1.6.3将用户异常添加到致命列表的一个方便方法是将ConditionalRejectingErrorHandler子类化。DefaultExceptionStrategy并覆盖方法isUserCauseFatal(Throwable cause),对于致命异常返回true。
3.1.14 Transactions
Introduction
Spring Rabbit框架在同步和异步用例中支持自动事务管理,可以声明性地选择许多不同的语义,Spring事务的现有用户对此很熟悉。这使得许多即使不是最常见的消息传递模式也非常容易实现。
有两种方法可以向框架发出所需事务语义的信号。在RabbitTemplate和SimpleMessageListenerContainer中都有一个标志channelTransacted ,如果为真,它告诉框架使用一个事务通道,并根据结果使用提交或回滚来结束所有操作(发送或接收),但有一个异常发出回滚信号。另一个信号是使用Spring的PlatformTransactionManager实现之一提供一个外部事务作为正在进行的操作的上下文。如果框架在发送或接收消息时已经有事务在进行中,且channelTransacted
标志为true,则消息传递事务的提交或回滚将延迟到当前事务的末尾。如果channelTransacted
标志为false,则没有事务语义应用于消息传递操作(它是自动标记的)。
channelTransacted 标志是一个配置时间设置:它在创建AMQP组件时声明并处理一次,通常是在应用程序启动时。原则上,外部事务更加动态,因为系统在运行时响应当前线程状态,但在实践中,当事务以声明的方式分层到应用程序时,通常也是一个配置设置。
对于使用RabbitTemplate的同步用例,外部事务由调用者提供,根据个人喜好(通常的Spring事务模型)以声明或命令的方式提供。声明式方法的一个例子(通常是首选的,因为它是非侵入性的),其中模板已经配置了channelTransacted=true:
@Transactional public void doSomething() { String incoming = rabbitTemplate.receiveAndConvert(); // do some more database processing... String outgoing = processInDatabaseAndExtractReply(incoming); rabbitTemplate.convertAndSend(outgoing); }
字符串有效负载作为标记为@Transactional的方法中的消息体接收、转换和发送,因此,如果数据库处理异常失败,传入的消息将返回给代理,而传出的消息将不发送。这适用于在事务方法链中使用RabbitTemplate的任何操作(例如,除非直接操纵通道以尽早提交事务)。
对于带有SimpleMessageListenerContainer的异步用例,如果需要外部事务,则必须由容器在设置侦听器时请求它。为了表示需要外部事务,用户在配置容器时向容器提供了PlatformTransactionManager的实现。
@Configuration public class ExampleExternalTransactionAmqpConfiguration { @Bean public SimpleMessageListenerContainer messageListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setTransactionManager(transactionManager()); container.setChannelTransacted(true); container.setQueueName("some.queue"); container.setMessageListener(exampleListener()); return container; } }
在上面的示例中,事务管理器被添加为从另一个bean定义注入的依赖项(未显示),channelTransacted 标志也被设置为true。其结果是,如果侦听器异常失败,事务将回滚,消息也将返回给代理。重要的是,如果事务未能提交(例如数据库约束错误或连接问题),那么AMQP事务也将回滚,消息将返回给代理。这有时被称为“最佳工作1阶段提交”,是一种非常强大的可靠消息传递模式。如果channelTransacted标志设置为false在上面的示例中,默认,然后外部事务仍将提供侦听器,但所有消息操作将auto-acked,所以效果提交消息即使在业务操作的回滚操作。
Conditional Rollback
在1.6.6版本之前,当使用外部事务管理器(例如JDBC)时,在容器的transactionAttribute中添加回滚规则没有效果;异常总是回滚事务。
此外,当在容器的通知链中使用事务通知时,条件回滚不是很有用,因为所有侦听器异常都包装在ListenerExecutionFailedException中。第一个问题已经纠正,规则现在得到了正确的应用。此外,现在还提供了ListenerFailedRuleBasedTransactionAttribute;它是RuleBasedTransactionAttribute的一个子类,唯一的区别是它知道ListenerExecutionFailedException并为规则使用此类异常的原因。此事务属性可以直接在容器中使用,也可以通过事务通知使用。
@Bean public AbstractMessageListenerContainer container() { ... container.setTransactionManager(transactionManager); RuleBasedTransactionAttribute transactionAttribute = new ListenerFailedRuleBasedTransactionAttribute(); transactionAttribute.setRollbackRules(Collections.singletonList( new NoRollbackRuleAttribute(DontRollBackException.class))); container.setTransactionAttribute(transactionAttribute); ... }
A note on Rollback of Received Messages
略。。。
Using the RabbitTransactionManager
略。。。
3.1.15 Message Listener Container Configuration
配置与事务和服务质量相关的SimpleMessageListenerContainer有相当多的选项,其中一些选项彼此交互。
下表显示了使用名称空间配置<rabbit:listener-container/>时容器属性名及其等效属性名(括号内)
有些属性没有通过名称空间公开;属性由N/A表示。
Table 3.3. Configuration options for a message listener container
Property (Attribute) | Description |
---|---|
(group) | 这仅在使用名称空间时可用。当指定时,Collection<MessageListenerContainer>的bean将用这个名称注册,每个<listener/>元素的容器将添加到集合中。例如,这允许通过遍历集合来启动/停止容器组。如果多个<listener-container/>元素具有相同的组值,则集合中的容器是指定的所有容器的集合。 |
channelTransacted (channel-transacted) | 布尔标志,指示在事务中应确认所有消息(手动或自动) |
acknowledgeMode (acknowledge) |
|
transactionManager (transaction-manager) | 用于监听器操作的外部事务管理器。还补充到channelTransacted—如果Channelis transacted,那么它的事务将与外部事务同步。 |
prefetchCount (prefetch) | 在一个套接字帧中从代理接收的消息数。这个值越高,消息的传递速度就越快,但是非顺序处理的风险就越大。如果致谢符号为NONE,则忽略。如果需要,将增加这个选项以匹配txSize。 |
shutdownTimeout (N/A) | 当容器关闭时(例如,如果其封闭的ApplicationContext关闭),它将等待正在运行的消息处理到这个限制。默认为5秒。 |
| |
forceCloseChannel (N/A) | 如果消费者在shutdownTimeout内没有响应关机,如果这是真的,那么通道将被关闭,从而导致任何未被应答的消息被重新请求。默认的错误。 |
txSize (transaction-size) | 当与确认acknowledgeMode AUTO一起使用时,容器将尝试在发送ack之前处理最多的消息数量(等待每个消息达到接收超时设置)。这也是提交事务通道的时候。如果prefetchCount小于txSize,它将被增加以匹配txSize。 |
receiveTimeout (receive-timeout) | 等待每条消息的最长时间。如果acknowledgeMode=NONE,则影响非常小——容器只是旋转并请求另一条消息。对于txSize为> 1的事务通道,它的影响最大,因为它会导致在超时过期之前,已经使用的消息不会被确认。 |
autoStartup (auto-startup) | 标志,指示容器应该在ApplicationContext启动时启动(作为所有bean初始化后发生的SmartLifecyclecallbacks的一部分)。默认值为true,但是如果您的代理在启动时可能不可用,则将其设置为false,然后当您知道代理已经准备好时,稍后手动调用start()。 |
phase (phase) | 当autoStartup为真时,此容器应在其中启动和停止的生命周期阶段。值越低,此容器启动得越早,停止得越晚。默认值是Integer。MAX_VALUE意味着容器将尽可能晚地启动并尽快停止。 |
adviceChain (advice-chain) | 要应用于侦听器执行的AOP建议数组。这可以用于应用额外的横切关注点,比如在代理死亡时自动重试。注意,只要代理仍然存在,CachingConnectionFactory将处理AMQP错误之后的简单重新连接。 |
taskExecutor (task-executor) | 对执行侦听器调用程序的Spring TaskExecutor(或标准JDK 1.5+ Executor)的引用。默认是一个SimpleAsyncTaskExecutor,使用内部托管线程。 |
errorHandler (error-handler) | 对ErrorHandler策略的引用,该策略用于处理MessageListener执行期间可能发生的任何未捕获异常。默认值:ConditionalRejectingErrorHandler |
concurrentConsumers (concurrency) |
初始化每个侦听器的并发使用者数量。参见3.1.16节“侦听器并发性”。 |
maxConcurrentConsumers (max-concurrency) |
如果需要,按需启动的并发消费者的最大数量。必须大于或等于concurrentConsumers。参见3.1.16节“侦听器并发性”。 |
concurrency (N/A) |
每个侦听器的并发消费者范围(min, max)。如果只提供n,则n是固定数量的消费者。参见3.1.16节“侦听器并发性”。 |
consumerStartTimeout (N/A) | 等待消费者线程启动的时间(以毫秒为单位)。如果此时间流逝,则写入错误日志;发生这种情况的一个例子是,如果taskExecutor配置的线程不足以支持容器concurrentConsumers。参见“线程和异步使用者”一节。默认60000(60秒)。 |
startConsumerMin Interval (min-start-interval) |
按需启动每个新消费者之前必须经过的以毫秒为单位的时间。参见3.1.16节“侦听器并发性”。默认10000(10秒)。 |
stopConsumerMinInterval (min-stop-interval) | 当检测到空闲用户时,从最后一个用户停止到该用户停止前必须经过的以毫秒为单位的时间。参见3.1.16节“侦听器并发性”。默认60000(1分钟)。 |
consecutiveActiveTrigger (min-consecutive-active) |
考虑启动新使用者时,使用者接收到的连续消息的最小数量,且不发生接收超时。也受到txSize的影响。参见3.1.16节“侦听器并发性”。默认10。 |
consecutiveIdleTrigger (min-consecutive-idle) | 在考虑停止使用者之前,使用者必须经历的最少接收超时次数。也受到txSize的影响。参见3.1.16节“侦听器并发性”。默认10。 |
connectionFactory (connection-factory) | 指向ConnectionFactory的引用;在使用XML名称空间配置时,默认引用的bean名称是“rabbitConnectionFactory”。 |
defaultRequeueRejected (requeue-rejected) |
确定是否应重新请求因侦听器抛出异常而被拒绝的消息。默认TRUE。 |
recoveryInterval (recovery-interval) |
确定尝试启动使用者之间的时间(以毫秒为单位),如果使用者因非致命原因无法启动。默认的5000。与recovery backback相互排斥。 |
recoveryBackOff (recovery-back-off) |
指定在尝试启动使用者之间的间隔(如果使用者因非致命原因而无法启动)的回退。默认设置为每5秒无限重试一次。与recoveryInterval相互排斥。 |
exclusive (exclusive) | 确定此容器中的单个使用者是否具有对队列的独占访问权。如果为真,则容器的并发性必须为1。如果另一个使用者具有独占访问权,容器将根据恢复间隔或恢复回退尝试恢复使用者。当使用名称空间时,此属性将与队列名称一起出现在<rabbit:listener/>元素上。默认false。 |
rabbitAdmin (admin) | 当侦听器容器侦听至少一个自动删除队列,并且在启动期间发现该队列丢失时,容器使用RabbitAdmin来声明队列以及任何相关的绑定和交换。如果将这些元素配置为使用条件声明(请参阅“条件声明”一节),则容器必须使用配置为声明这些元素的admin。在这里指定admin;只有在使用带有条件声明的自动删除队列时才需要。如果您不希望在容器启动之前声明自动删除队列,请在管理中将自动启动设置为false。默认为RabbitAdmin,它将声明所有非条件元素。 |
missingQueuesFatal (missing-queues-fatal) | 从1.3.5版本开始,SimpleMessageListenerContainer具有这个新属性。
当设置为true(缺省值)时,如果代理上没有可用的配置队列,则认为这是致命的。这将导致应用程序上下文在启动期间初始化失败;此外,当队列在容器运行时被删除时,默认情况下,使用者会三次重试连接到队列(间隔5秒),如果这些尝试失败,则会停止容器。
这在以前的版本中是不可配置的。 当设置为false时,在进行了3次重试之后,容器将进入恢复模式,与其他问题一样,比如代理关闭。容器将尝试根据recoveryInterval属性进行恢复。在每次恢复尝试期间,每个使用者将再次尝试4次,以5秒为间隔被动地声明队列。这一进程将无限期地继续下去。
您还可以使用properties bean为所有容器全局设置属性,如下所示: <util:properties id="spring.amqp.global.properties"> <prop key="smlc.missing.queues.fatal">false</prop> </util:properties> 此全局属性不会应用于任何具有显式missingQueuesFatal属性集的容器。 可以使用下面的属性覆盖默认的重试属性(每隔5秒重试3次)。 |
mismatchedQueuesFatal (mismatched-queues-fatal) | 这是在1.6版中添加的。当容器启动时,如果该属性为true(缺省值:false),容器将检查上下文中声明的所有队列是否与代理上已经存在的队列兼容。如果存在不匹配的属性(例如自动删除)或参数(例如x-message-ttl),容器将视为致命异常(和应用程序上下文)无法启动。 如果在恢复过程中检测到问题(例如在连接丢失之后),容器将被停止。 应用程序上下文中必须有一个RabbitAdmin(或在容器上使用RabbitAdmin属性专门配置的一个);否则,此属性必须为false。 如果代理在初始启动期间不可用,则容器将启动,并在建立连接时检查条件。 重要:检查针对上下文中的所有队列执行,而不仅仅是配置特定侦听器要使用的队列。如果希望将检查限制为容器所使用的队列,则应该为容器配置一个单独的RabbitAdmin,并使用RabbitAdmin属性提供对它的引用。有关更多信息,请参见“条件声明”一节。 |
possibleAuthenticationFailureFatal (possible-authentication-failure-fatal) | 当设置为true(默认值)时,如果在连接期间抛出一个PossibleAuthenticationFailureException ,则认为它是致命的。这将导致应用程序上下文在启动期间初始化失败。
自1.7.4版本。
当设置为false时,在进行了3次重试之后,容器将进入恢复模式,与其他问题一样,比如代理关闭。容器将尝试根据recoveryInterval属性进行恢复。在每次恢复尝试期间,每个消费者将再次尝试启动4次。这一进程将无限期地继续下去。 |
autoDeclare (auto-declare) | 从1.4版本开始,SimpleMessageListenerContainer就有了这个新属性。
设置为true(缺省值)时,容器将使用RabbitAdmin重新定义所有AMQP对象(队列、交流绑定),如果它检测到至少一个队列缺少创业期间,也许因为它是auto-delete或过期的队列,但redeclaration将继续如果队列是失踪的任何理由。若要禁用此行为,请将此属性设置为false。注意,如果容器的所有队列都丢失了,那么容器将无法启动。 在版本1.6之前,如果上下文中有多个管理员,容器将随机选择一个。如果没有管理员,它将在内部创建一个管理员。无论哪种情况,这都可能导致意想不到的结果。从版本1.6开始,要让autoDeclare工作,上下文中必须只有一个RabbitAdmin,或者必须使用RabbitAdmin属性在容器上配置对特定实例的引用。 |
declarationRetries (declaration-retries) | 从1.4.3、1.3.9版本开始,SimpleMessageListenerContainer具有这个新属性。namespace属性在1.5.x版本中可用
被动队列声明失败时重试的次数。被动队列声明发生在使用者启动时,或者当从多个队列进行消费时,当初始化期间并非所有队列都可用时。当重试耗尽后,不能被动地声明任何已配置的队列时(无论出于何种原因),容器行为由上面的“missingQueuesFatal”属性控制。默认值:3次重试(4次尝试)。 |
failedDeclarationRetryInterval (failed-declaration-retry- interval) | 从1.4.3、1.3.9版本开始,SimpleMessageListenerContainer具有这个新属性。namespace属性在1.5.x版本中可用
被动队列声明重试尝试之间的间隔。被动队列声明发生在使用者启动时,或者当从多个队列进行消费时,当初始化期间并非所有队列都可用时。默认值:5000(5秒)。 |
retryDeclarationInterval (missing-queue-retry- interval) | 从1.4.3、1.3.9版本开始,SimpleMessageListenerContainer具有这个新属性。namespace属性在1.5.x版本中可用
如果配置队列的子集在使用者初始化期间可用,则使用者将从这些队列开始消费。使用者将尝试使用此间隔被动地声明丢失的队列。当这个间隔过期时,声明重试和失败声明重试间隔将再次使用。如果仍然缺少队列,消费者将再次等待这个间隔,然后再试一次。此过程将无限期地继续,直到所有队列都可用为止。默认值:60000(1分钟)。 |
consumerTagStrategy (consumer-tag-strategy) | 从1.4.5版本开始,SimpleMessageListenerContainer具有这个新属性。namespace属性在1.5.x版本中可用
以前,只能使用经纪公司生成的消费者标签;虽然这仍然是默认值,但现在可以提供ConsumerTagStrategy的实现,为每个消费者创建(惟一的)标签。 |
idleEventInterval (idle-event-integer) | 从1.6版开始,SimpleMessageListenerContainer就有了这个新属性。请参阅“检测空闲异步使用者”一节。 |
alwaysRequeueWithTx ManagerRollback (N/A) | 在配置事务管理器时,设置为true总是在回滚时请求requeue 消息(默认)。设置为false来配置容器,以使用一致的消息拒绝行为,而不管是否配置了事务管理器。 |
3.1.16 Listener Concurrency
默认情况下,侦听器容器将启动单个使用者,该使用者将接收来自队列的消息。
在检查前一节中的表时,您将看到许多控制并发性的属性。最简单的是concurrentConsumers,它简单地创建一个(固定的)消费者数量,该数量的消费者将同时处理消息。
在1.3.0版本之前,这是惟一可用的设置,容器必须停止并重新启动才能更改设置。
自1.3.0版本以来,您现在可以动态调整concurrentConsumers属性。如果在容器运行时对其进行了更改,则将根据需要添加或删除使用者,以适应新的设置。
此外,添加了一个新的属性maxConcurrentConsumers,容器将根据工作负载动态调整并发性。这与四个附加属性一起工作:consecutiveActiveTrigger、startConsumerMinInterval、consecutiveIdleTrigger、stopConsumerMinInterval。在默认设置下,增加消费者的算法如下:
如果尚未到达maxConcurrentConsumers,并且现有的使用者连续10个周期处于活动状态,并且自上次启动使用者以来至少已经过了10秒,则启动一个新的使用者。如果使用者接收到至少一条以txSize * receiveTimeout毫秒为单位的消息,则认为该使用者处于活动状态。
在默认设置下,减少消费者的算法如下:
如果有多个concurrentConsumers 正在运行,并且一个消费者检测到连续10个超时(空闲),并且最后一个消费者至少在60秒之前被停止,那么该消费者将被停止。超时取决于receiveTimeout和txSize属性。如果使用者没有收到txSize * receiveTimeout毫秒中的消息,则认为该使用者处于空闲状态。因此,在默认超时(1秒)和txSize为4的情况下,在40秒的空闲时间之后将考虑停止使用者(4个超时对应一个空闲检测)。
实际上,只有当整个容器空闲一段时间时,才会停止使用者。这是因为代理将在所有活动消费者之间共享其工作。
3.1.17 Exclusive Consumer 唯一消费者
同样从版本1.3开始,侦听器容器可以配置为一个独占消费者;这可以防止其他容器在当前使用者被取消之前从队列中消费。这样一个容器的并发性必须是1。
当使用独占消费者时,其他容器将根据recoveryInterval属性尝试从队列中消费,如果尝试失败,将记录一个警告。
3.1.18 Listener Container Queues
版本1.3为处理侦听器容器中的多个队列引入了许多改进。
容器必须配置为监听至少一个队列;以前也是这样,但是现在可以在运行时添加和删除队列。在处理任何预获取的消息时,容器将回收(取消和重新创建)使用者。参见方法addQueues、addQueueNames、removeQueues和removeQueueNames。删除队列时,至少必须保留一个队列。
消费者现在将在其任何队列可用时启动—以前,如果任何队列不可用,容器将停止。现在,只有在没有队列可用时才会出现这种情况。如果不是所有队列都可用,容器将尝试每60秒被动地声明(并使用)丢失的队列。
此外,如果使用者从代理接收到取消(例如,如果队列被删除),使用者将尝试恢复,而恢复的使用者将继续处理来自任何其他配置队列的消息。以前,一个队列上的cancel取消了整个消费者,最终容器将由于缺少队列而停止。
如果希望永久删除队列,应该在删除队列之前或之后更新容器,以避免将来尝试使用队列。
3.1.19 Resilience: Recovering from Errors and Broker Failures 弹性:从错误和代理失败中恢复
Introduction
Spring AMQP提供的一些关键(也是最流行的)高级特性用于在协议错误或代理失败时进行恢复和自动重新连接。我们已经在本指南中看到了所有相关组件,但是将它们放在一起并单独列出特性和恢复场景应该会有所帮助。
主要的重连接功能由CachingConnectionFactory本身启用。使用RabbitAdmin自动声明特性通常也是有益的。此外,如果您关心保证交付,您可能还需要在RabbitTemplate和SimpleMessageListenerContainer中使用channelTransacted 标志,还需在SimpleMessageListenerContainer中执行AcknowledgeMode.AUTO(如果您自己执行应答,则为manual )。
Automatic Declaration of Exchanges, Queues and Bindings
RabbitAdmin组件可以在启动时声明交换、队列和绑定。它通过ConnectionListener懒惰地完成这一操作,因此,如果代理在启动时不存在,也没有关系。第一次使用连接时(例如通过发送消息),侦听器将启动,并应用管理功能。在侦听器中执行自动声明的另一个好处是,如果连接因任何原因被删除(例如代理死亡、网络故障等),则在重新建立连接时将再次应用它们。
以这种方式声明的队列必须具有固定的名称;匿名队列是不持久的、独占的和自动删除的。
重要:只有当CachingConnectionFactory缓存模式为CHANNEL(默认)时才会执行自动声明。存在此限制是因为排他队列和自动删除队列被绑定到连接。
See also the section called “RabbitMQ Automatic Connection/Topology recovery”.
Failures in Synchronous Operations and Options for Retry
如果您使用RabbitTemplate(例如)在同步序列中丢失到代理的连接,那么Spring AMQP将抛出AmqpException(通常但不总是AmqpIOException)。我们不试图隐藏存在问题的事实,所以您必须能够捕获并响应异常。如果您怀疑连接丢失,而这不是您的错,那么最简单的方法就是再次尝试该操作。您可以手动执行此操作,或者您可以考虑使用Spring Retry来处理重试(命令式或声明式)。
Spring Retry提供了两个AOP拦截器,并提供了很大的灵活性来指定重试的参数(尝试次数、异常类型、后退算法等)。Spring AMQP还提供了一些方便的工厂bean,以方便的形式为AMQP用例创建Spring重试拦截器,使用强类型的回调接口实现自定义恢复逻辑。有关详细信息,请参阅StatefulRetryOperationsInterceptor和StatelessRetryOperationsInterceptor的Javadocs和属性。如果没有事务或事务在重试回调函数中启动,则使用无状态重试是合适的。请注意,无状态重试比有状态重试更容易配置和分析,但如果必须回滚或肯定要回滚正在进行的事务,则通常不合适。事务中间的断开连接应该具有与回滚相同的效果,因此对于在堆栈较高位置启动事务的重新连接,有状态重试通常是最佳选择。
从1.3版开始,提供了一个builder API来帮助使用Java(或@Configuration类)组装这些拦截器,例如:
@Bean public StatefulRetryOperationsInterceptor interceptor() { return RetryInterceptorBuilder.stateful() .maxAttempts(5) .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval .build(); }
只有一部分重试功能可以这样配置;更高级的特性需要将RetryTemplate配置为Spring bean。有关可用策略及其配置的完整信息,请参阅Spring Retry Javadocs。
Message Listeners and the Asynchronous Case
如果MessageListener由于业务异常而失败,则异常由消息侦听器容器处理,然后返回侦听另一条消息。如果失败是由断开的连接(不是业务异常)引起的,则必须取消并重新启动为侦听器收集消息的使用者。SimpleMessageListenerContainer无缝地处理这个问题,它会留下一个日志,说明正在重新启动侦听器。事实上,它会无休止地循环试图重新启动消费者,只有当消费者的行为确实非常恶劣时,它才会放弃。一个副作用是,如果容器启动时代理处于关闭状态,它将一直尝试,直到能够建立连接为止。
与协议错误和断开的连接相反,业务异常处理可能需要更多的考虑和一些定制配置,特别是在使用事务和/或容器ack时。2.8之前。RabbitMQ没有死信行为的定义,因此在默认情况下,由于业务异常而被拒绝或回滚的消息可以无限地重新发送。要在客户机中限制重新交付的数量,一种选择是侦听器的通知链中的StatefulRetryOperationsInterceptor。拦截器可以有一个实现自定义死信操作的恢复回调:适合您的特定环境的任何操作。
另一种方法是将容器的rejectRequeued属性设置为false。这将导致丢弃所有失败的消息。当使用RabbitMQ 2.8时。x或更高,这也有助于将消息传递给死信交换。
或者,您可以抛出AmqpRejectAndDontRequeueException;无论defaultRequeueRejected
属性的设置如何,这都会防止消息请求。
通常,将使用这两种技术的组合。在通知链中使用StatefulRetryOperationsInterceptor,其中MessageRecover抛出AmqpRejectAndDontRequeueException。当所有重试都完成后,将调用MessageRecover。默认MessageRecoverer只使用错误消息并发出警告消息。在这种情况下,消息被打包,并且不会被发送到死信交换(如果有的话)。
从1.3版开始,提供了一个新的RepublishMessageRecoverer,允许在重试耗尽后发布失败的消息:
@Bean RetryOperationsInterceptor interceptor() { return RetryInterceptorBuilder.stateless() .maxAttempts(5) .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "bar", "baz")) .build(); }
RepublishMessageRecoverer在消息头中发布带有附加信息的消息,例如异常消息、堆栈跟踪、原始交换和路由键。可以通过创建子类并覆盖additionalHeaders()来添加额外的头。
Exception Classification for Retry 重试的异常分类
Spring Retry在确定哪些异常可以调用Retry方面具有很大的灵活性。默认配置将重试所有异常。考虑到用户异常将被包装在ListenerExecutionFailedException中,我们需要确保分类检查异常的原因。默认分类器只查看顶层异常。
从Spring Retry 1.0.3开始,BinaryExceptionClassifier就有一个traverseCauses属性(默认为false)。当为真时,它将遍历异常原因,直到找到匹配或没有原因。
要使用此分类器进行重试,请使用SimpleRetryPolicy,该策略由构造函数创建,该构造函数接受最大尝试、异常映射和boolean (traverseCauses),并将此策略注入RetryTemplate。
3.1.20 Debugging
Spring AMQP提供了广泛的日志记录功能,尤其是在调试级别。
如果希望监视应用程序和代理之间的AMQP协议,可以使用WireShark这样的工具,它有一个插件来解码协议。另外,RabbitMQ java客户机附带了一个非常有用的类Tracer。默认情况下,当作为主服务器运行时,它监听端口5673并连接到本地主机上的端口5672。只需运行它,并更改连接工厂配置,以便在本地主机上连接到端口5673。它在控制台上显示解码协议。有关更多信息,请参考Tracer
javadocs。
3.3 Sample Applications
3.3.1 Introduction
Spring AMQP样例项目包括两个样例应用程序。第一个示例是一个简单的“Hello World”示例,它演示了同步和异步消息接收。它为理解基本组件提供了一个很好的起点。第二个示例基于一个股票交易用例来演示在实际应用程序中常见的交互类型。在本章中,我们将提供每个示例的快速浏览,以便您能够集中精力于最重要的组件。这些示例都基于maven,因此您应该能够将它们直接导入任何支持maven的IDE(例如SpringSource工具套件)。
3.3.2 Hello World
Introduction
Hello World示例演示了同步和异步消息接收。您可以将spring-rabbit-helloworld样例导入IDE,然后遵循下面的讨论。
Synchronous Example
在src/main/java目录中,导航到org.springframework.amqp.helloworld包。打开HelloWorldConfiguration类,注意它包含类级别的@Configuration注释和方法级别的一些@Bean注释。这是Spring基于java的配置的一个例子。你可以在这里阅读更多。
@Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); return connectionFactory; }
配置还包含一个RabbitAdmin实例,该实例默认情况下查找Exchange、Queue或Binding类型的任何bean,然后在代理上声明它们。事实上,在HelloWorldConfiguration中生成的“helloWorldQueue”bean就是一个例子,因为它是队列的一个实例。
@Bean public Queue helloWorldQueue() { return new Queue(this.helloWorldQueueName); }
回顾“rabbitTemplate”bean配置,您将看到helloWorldQueue的名称被设置为它的“queue”属性(用于接收消息)和“routingKey”属性(用于发送消息)。
现在我们已经研究了配置,让我们看看实际使用这些组件的代码。首先,从同一个包中打开Producer类。它包含一个main()方法,其中创建了Spring ApplicationContext。
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(RabbitConfiguration.class); AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class); amqpTemplate.convertAndSend("Hello World"); System.out.println("Sent: Hello World"); }
正如您在上面的示例中所看到的,AmqpTemplate bean被检索并用于发送消息。因为客户端代码应该尽可能依赖于接口,所以类型是AmqpTemplate而不是RabbitTemplate。尽管在HelloWorldConfiguration中创建的bean是RabbitTemplate的一个实例,但是依赖于接口意味着这段代码更具可移植性(配置可以独立于代码进行更改)。由于调用了convertAndSend()方法,模板将委托给它的MessageConverter实例。在本例中,它使用默认的SimpleMessageConverter,但是可以为HelloWorldConfiguration中定义的“rabbitTemplate”bean提供不同的实现。
现在打开Consumer类。它实际上共享相同的配置基类,这意味着它将共享“rabbitTemplate”bean。这就是为什么我们用“routingKey”(用于发送)和“queue”(用于接收)配置该模板。正如您在3.1.4节“AmqpTemplate”中看到的,您可以将routingKey参数传递给send方法,将队列参数传递给receive方法。消费者代码基本上是生产者的镜像,调用receiveAndConvert()而不是convertAndSend()。
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(RabbitConfiguration.class); AmqpTemplate amqpTemplate = context.getBean(AmqpTemplate.class); System.out.println("Received: " + amqpTemplate.receiveAndConvert()); }
如果运行生产者,然后运行消费者,应该会在控制台输出中看到“Received: Hello World”消息。
Asynchronous Example
现在我们已经浏览了同步Hello World示例,现在是时候转向一个稍微高级一些但功能强大得多的选项了。经过一些修改,Hello World示例可以提供异步接收的示例,即消息驱动的pojo。实际上,有一个子包提供了以下内容:org.springframework. amqm .samples.helloworld.async。
我们将再次从发送方开始。打开ProducerConfiguration类,注意它创建了一个“connectionFactory”和“rabbitTemplate”bean。这一次,由于配置专用于消息发送端,所以我们甚至不需要任何队列定义,而且RabbitTemplate只设置了routingKey属性。AMQP缺省交换是一个没有名称的直接交换。所有队列都绑定到该缺省交换器,并将其名称作为路由键。这就是为什么我们只需要在这里提供路由键。
public RabbitTemplate rabbitTemplate() { RabbitTemplate template = new RabbitTemplate(connectionFactory()); template.setRoutingKey(this.helloWorldQueueName); return template; }
由于这个示例将演示异步消息接收,所以生产端被设计为连续发送消息(如果它是一个类似同步版本的每次执行消息模型,那么它实际上是一个消息驱动的消费者就不那么明显了)。负责连续发送消息的组件被定义为ProducerConfiguration中的一个内部类。它被配置为每3秒执行一次。
static class ScheduledProducer { @Autowired private volatile RabbitTemplate rabbitTemplate; private final AtomicInteger counter = new AtomicInteger(); @Scheduled(fixedRate = 3000) public void sendMessage() { rabbitTemplate.convertAndSend("Hello World " + counter.incrementAndGet()); } }
您不需要了解所有的细节,因为真正的重点应该放在接收端(我们将在稍后讨论)。但是,如果您还不熟悉Spring 3.0任务调度支持,您可以在这里了解更多。简言之,ProducerConfiguration中的“后处理器”bean正在向调度程序注册任务。
现在,让我们转向接收方。为了强调消息驱动的POJO行为,将从响应消息的组件开始。这个类称为HelloWorldHandler。
public class HelloWorldHandler { public void handleMessage(String text) { System.out.println("Received: " + text); } }
很明显,这是POJO。它不扩展任何基类,不实现任何接口,甚至不包含任何导入。它被Spring AMQP MessageListenerAdapter“调整”到MessageListener接口。然后可以在SimpleMessageListenerContainer上配置该适配器。对于这个示例,容器是在ConsumerConfiguration类中创建的。您可以在那里看到封装在适配器中的POJO。
SimpleMessageListenerContainer是一个Spring生命周期组件,默认情况下将自动启动。如果您查看Consumer类,您将看到它的main()方法仅由一行引导程序组成,用于创建ApplicationContext。生产者的main()方法也是一个单行引导程序,因为其方法被@Scheduled注释的组件也将自动开始执行。您可以以任何顺序启动生产者和消费者,您应该看到每3秒发送和接收一次消息。
3.4 Testing Support
3.4.1 Introduction
为异步应用程序编写集成一定要比测试简单的应用程序复杂。当抽象(比如@RabbitListener注释)出现时,这就变得更加复杂了。问题是如何验证发送消息后,侦听器是否按预期接收了消息。
框架本身有很多单元测试和集成测试;一些使用mock,另一些使用带有活动RabbitMQ代理的集成测试。您可以参考这些测试来获得测试场景的一些想法。
3.4.3 @RabbitListenerTest and RabbitListenerTestHarness
用@RabbitListenerTest注释一个@Configuration类将导致框架用一个子类RabbitListenerTestHarness替换标准的RabbitListenerAnnotationBeanPostProcessor(它还将通过@EnableRabbit启用@RabbitListener检测)。
RabbitListenerTestHarness以两种方式增强了侦听器——它将侦听器封装在Mockito Spy中,从而支持正常的Mockito存桩和验证操作。它还可以向侦听器添加一个建议,以便访问抛出的参数、结果和异常。您可以通过@RabbitListenerTest上的属性来控制启用了哪些(或两者都启用)。后者用于访问调用的低层数据——它还支持在调用异步侦听器之前阻塞测试线程。
重要:不能监视或建议final 的@RabbitListener方法;此外,只有具有id属性的侦听器才能被监视或通知
让我们看一些例子。
Using spy:
@Configuration @RabbitListenerTest public class Config { @Bean public Listener listener() { return new Listener(); } ... } public class Listener { @RabbitListener(id="foo", queues="#{queue1.name}") public String foo(String foo) { return foo.toUpperCase(); } @RabbitListener(id="bar", queues="#{queue2.name}") public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { ... } } public class MyTests { @Autowired private RabbitListenerTestHarness harness; @Test public void testTwoWay() throws Exception { assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")); Listener listener = this.harness.getSpy("foo"); assertNotNull(listener); verify(listener).foo("foo"); } @Test public void testOneWay() throws Exception { Listener listener = this.harness.getSpy("bar"); assertNotNull(listener); LatchCountDownAndCallRealMethodAnswer answer = new LatchCountDownAndCallRealMethodAnswer(2); doAnswer(answer).when(listener).foo(anyString(), anyString()); this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar"); this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz"); assertTrue(answer.getLatch().await(10, TimeUnit.SECONDS)); verify(listener).foo("bar", this.queue2.getName()); verify(listener).foo("baz", this.queue2.getName()); } }
把安全带插入测试用例中,这样我们就可以访问spy了。
获取对该间谍的引用,以便我们可以验证它是否按预期调用。由于这是一个发送和接收操作,所以不需要挂起测试线程,因为它已经挂起在RabbitTemplate中等待响应。
在本例中,我们只使用了send操作,因此需要一个锁存器来等待容器线程上对侦听器的异步调用。我们使用其中一个Answer<?>实现对此有所帮助。
使用捕捉建议:
@Configuration @ComponentScan @RabbitListenerTest(spy = false, capture = true) public class Config { } @Service public class Listener { private boolean failed; @RabbitListener(id="foo", queues="#{queue1.name}") public String foo(String foo) { return foo.toUpperCase(); } @RabbitListener(id="bar", queues="#{queue2.name}") public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { if (!failed && foo.equals("ex")) { failed = true; throw new RuntimeException(foo); } failed = false; } } public class MyTests { @Autowired private RabbitListenerTestHarness harness; @Test public void testTwoWay() throws Exception { assertEquals("FOO", this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")); InvocationData invocationData = this.harness.getNextInvocationDataFor("foo", 0, TimeUnit.SECONDS); assertThat(invocationData.getArguments()[0], equalTo("foo")); assertThat((String) invocationData.getResult(), equalTo("FOO")); } @Test public void testOneWay() throws Exception { this.rabbitTemplate.convertAndSend(this.queue2.getName(), "bar"); this.rabbitTemplate.convertAndSend(this.queue2.getName(), "baz"); this.rabbitTemplate.convertAndSend(this.queue2.getName(), "ex"); InvocationData invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); Object[] args = invocationData.getArguments(); assertThat((String) args[0], equalTo("bar")); assertThat((String) args[1], equalTo(queue2.getName())); invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); args = invocationData.getArguments(); assertThat((String) args[0], equalTo("baz")); invocationData = this.harness.getNextInvocationDataFor("bar", 10, TimeUnit.SECONDS); args = invocationData.getArguments(); assertThat((String) args[0], equalTo("ex")); assertEquals("ex", invocationData.getThrowable().getMessage()); } }