Spring AMQP 随笔 1 Send & Receive

0. 想要改变时钟了

这几个章节,还算是比较简单,比较靠近 实际的编码,配置

4.1.5. Sending Messages

A better way of thinking about the exchange and routing key properties is that the explicit method parameters always override the template’s default values.
In fact, even if you do not explicitly set those properties on the template, there are always default values in place.

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

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();

Publisher Returns

When the template’s mandatory(命令的) property is true, returned messages are provided by the callback described in AmqpTemplate.

Batching

Only when a batch is complete is the message sent to RabbitMQ.

BatchingRabbitTemplate is a subclass of RabbitTemplate with an overridden send method that batches messages according to the BatchingStrategy.

public interface BatchingStrategy {
    MessageBatch addToBatch(String exchange, String routingKey, Message message);
    Date nextRelease();
    Collection<MessageBatch> releaseBatches();
}

Batched data is held in memory. Unsent messages can be lost in the event of a system failure.

Batched messages are automatically de-batched by listener containers by default (by using the springBatchFormat message header).
Rejecting any message from a batch causes the entire(整个) batch to be rejected.

4.1.6. Receiving Messages

Message reception(接收) is always a little more complicated(复杂) than sending.
There are two ways to receive a Message.

  • The simpler option is to poll for one Message at a time with a polling method call.
  • The more complicated yet(还,但,却) more common approach is to register a listener that receives Messages on-demand, asynchronously.

Polling Consumer

The AmqpTemplate itself can be used for polled Message reception.
By default, if no message is available, null is returned immediately. There is no blocking.
You can set a receiveTimeout, in milliseconds, and the receive methods block for up to that long, waiting for a message.
A value less than zero means block indefinitely (or at least until the connection to the broker is lost).
Version 1.6 introduced variants(变种) of the receive methods that allows the timeout be passed in on each call.

Since the receive operation creates a new QueueingConsumer for each message,
this technique is not really appropriate for high-volume environments.
Consider using an asynchronous consumer or a receiveTimeout of zero for those use cases.

Starting with version 2.4.8, when using a non-zero timeout, you can specify arguments passed into the basicConsume method used to associate the consumer with the channel.
For example: template.addConsumerArg(“x-priority”, 10).

the AmqpTemplate has some convenience methods for receiving POJOs instead of Message instances

Object receiveAndConvert() throws AmqpException;
Object receiveAndConvert(String queueName) throws AmqpException;
Object receiveAndConvert(long timeoutMillis) throws AmqpException;
Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException;

the AmqpTemplate has several convenience receiveAndReply methods for synchronously receiving, processing and replying to messages.

The AmqpTemplate implementation takes care of the receive and reply phases.
In most cases, you should provide only an implementation of ReceiveAndReplyCallback to perform some business logic for the received message
and build a reply object or message, if needed.
Note, a ReceiveAndReplyCallback may return null.
In this case, no reply is sent and receiveAndReply works like the receive method.
This lets the same queue be used for a mixture of messages, some of which may not need a reply.

Asynchronous Consumer

Spring AMQP also supports annotated listener endpoints through the use of the @RabbitListener annotation and provides an open infrastructure to register endpoints programmatically.
This is by far(迄今为止) the most convenient way to setup an asynchronous consumer.

The prefetch default value used to be 1, which could lead to under-utilization(利用不充分) of efficient consumers.
Starting with version 2.0, the default prefetch value is now 250, which should keep consumers busy in most common scenarios and thus improve throughput.
There are, nevertheless(但不限于), scenarios where the prefetch value should be low:

  • For large messages, especially(特别是) if the processing is slow (messages could add up to a large amount of memory in the client process)
  • When strict message ordering is necessary (the prefetch value should be set back to 1 in this case)
  • Other special cases

Also, with low-volume messaging and multiple consumers (including concurrency within a single listener container instance),
you may wish to reduce the prefetch to get a more even distribution of messages across consumers.

Message Listener

For asynchronous Message reception, a dedicated(专门的) component (not the AmqpTemplate) is involved.
That component is a container for a Message-consuming callback.
Starting with an implementation of the MessageListener interface.

If your callback logic depends on the AMQP Channel instance for any reason, you may instead use the ChannelAwareMessageListener.
It looks similar but has an extra parameter.

If you prefer to maintain a stricter separation between your application logic and the messaging API,
you can rely upon an adapter implementation that is provided by the framework.
This is often referred to as “Message-driven POJO” support.

MessageListenerAdapter listener = new MessageListenerAdapter(somePojo);
listener.setDefaultListenerMethod("myMethod");  // 通过反射解开强依赖

A more flexible mechanism for POJO messaging, the @RabbitListener annotation.

Container

Now that(既然) you have seen the various options for the Message-listening callback, we can turn our attention to the container.
Basically, the container handles the “active” responsibilities so that the listener callback can remain passive(保持被动).
The container is an example of a “lifecycle” component. It provides methods for starting and stopping.
When configuring the container, you essentially(本质上) bridge the gap between an AMQP Queue and the MessageListener instance.
You must provide a reference to the ConnectionFactory and the queue names or Queue instances from which that listener should consume messages.

@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 CachingConnectionFactory 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);
            }
        };
    }
}

Consumer Priority

The broker now supports consumer priority (see Using Consumer Priorities with RabbitMQ).
This is enabled by setting the x-priority argument on the consumer.
The SimpleMessageListenerContainer now supports setting consumer arguments.

container.setConsumerArguments(Collections.
<String, Object> singletonMap("x-priority", Integer.valueOf(10)));

auto-delete Queues

When a container is configured to listen to auto-delete queues, the queue has an x-expires option,
or the Time-To-Live policy is configured on the Broker, the queue is removed by the broker when the container is stopped
(that is, when the last consumer is cancelled).

The container uses a RabbitAdmin to redeclare any missing queues during startup.

You can also use conditional declaration together with an auto-startup="false" admin to defer(推迟) queue declaration until the container is started.

the queue and exchange are declared by containerAdmin, which has auto-startup="false" so that the elements are not declared during context initialization.
Also, the container is not started for the same reason. When the container is later started, it uses its reference to containerAdmin to declare the elements.

Batched Messages

Batched messages (created by a producer) are automatically de-batched by listener containers (using the springBatchFormat message header).
Rejecting any message from a batch causes the entire batch to be rejected.

Starting with version 2.2, the SimpleMessageListenerContainer can be use to create batches on the consumer side (where the producer sent discrete(离散) messages).

Set the container property consumerBatchEnabled to enable this feature.
deBatchingEnabled must also be true so that the container is responsible for processing batches of both types.
Implement BatchMessageListener or ChannelAwareBatchMessageListener when consumerBatchEnabled is true.
Starting with version 2.2.7 both the SimpleMessageListenerContainer and DirectMessageListenerContainer can debatch producer created batches as List.

Consumer Events

The containers publish application events whenever a listener (consumer) experiences a failure of some kind.

Consumer Tags

You can provide a strategy to generate consumer tags. By default, the consumer tag is generated by the broker.

public interface ConsumerTagStrategy {
    String createConsumerTag(String queue);
}

The queue is made available so that it can (optionally) be used in the tag.

Annotation-driven Listener Endpoints

@Component
public class MyService {
    @RabbitListener(queues = "myQueue")
    public void processOrder(String data) {
        ...
    }
}

The idea of the preceding example is that, whenever a message is available on the queue named myQueue,
the processOrder method is invoked accordingly (in this case, with the payload of the message).

The annotated endpoint infrastructure creates a message listener container behind the scenes for each annotated method, by using a RabbitListenerContainerFactory.

In the preceding example, myQueue must already exist and be bound to some exchange.
The queue can be declared and bound automatically, as long as a RabbitAdmin exists in the application context.

Property placeholders (${some.property}) or SpEL expressions (#{someExpression}) can be specified for the annotation properties (queues etc).
See Listening to Multiple Queues for an example of why you might use SpEL instead of a property placeholder.

@Component
public class MyService { 
    // a queue myQueue is declared automatically (durable) together with the exchange, 
    // if needed, and bound to the exchange with the routing key.
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "myQueue", durable = "true"),
        // binding to an existing exchange that might have different settings (ignoreDeclarationExceptions).
        // By default, the properties of an existing exchange must match
        exchange = @Exchange(value = "auto.exch", ignoreDeclarationExceptions = "true"),
        // you can now bind a queue to an exchange with multiple routing keys
        key = "orderRoutingKey")
    )
    public void processOrder(Order order) {
    ...
    }

    // an anonymous (exclusive, auto-delete) queue is declared and bound; the queue name is created by the framework using the Base64UrlNamingStrategy. 
    // You cannot declare broker-named queues using this technique; they need to be declared as bean definitions
    @RabbitListener(bindings = @QueueBinding(
        value = @Queue,
        exchange = @Exchange(value = "auto.exch"),
        key = "invoiceRoutingKey")
    )
    public void processInvoice(Invoice invoice) {
    ...
    }

    //  a queue with the name retrieved from property my.queue is declared, if necessary, 
    //  with the default binding to the default exchange using the queue name as the routing key.
    @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true"))
    public String handleWithSimpleDeclare(String data) {
      ...
    }

    // specify arguments within @QueueBinding annotations for queues, exchanges, and bindings
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "auto.headers", autoDelete = "true",
                    // The argument name, value, and type can be property placeholders or SpEL
                    // If a name resolves to null or an empty String, that @Argument is ignored.
                    arguments = @Argument(name = "x-message-ttl", value = "10000",
                            // The expression for type must resolve to a Class or the fully-qualified name of a class.
                            type = "java.lang.Integer")),
            exchange = @Exchange(value = "auto.headers", type = ExchangeTypes.HEADERS, autoDelete = "true"),
            arguments = {
                    @Argument(name = "x-match", value = "all"),
                    @Argument(name = "thing1", value = "somevalue"),
                    @Argument(name = "thing2")
            })
    )
    public String handleWithHeadersExchange(String foo) {
        ...
    }
}

Meta-annotations

Sometimes you may want to use the same configuration for multiple listeners. To reduce the boilerplate configuration,
you can use meta-annotations to create your own listener annotation.

@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) {
        ...
    }
}

@AliasFor is supported to allow overriding properties on the meta-annotated annotation.
Also, user annotations can now be @Repeatable

@Component
static class MetaAnnotationTestBean {

    @MyListener("queue1")
    @MyListener("queue2")
    public void handleIt(String body) {
    }

}


@RabbitListener
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyListeners.class)
static @interface MyListener {

    @AliasFor(annotation = RabbitListener.class, attribute = "queues")
    String[] value() default {};

}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
static @interface MyListeners {

    MyListener[] value();

}

Enable Listener Endpoint Annotations

To enable support for @RabbitListenerannotations, you can add @EnableRabbit to one of your @Configuration classes.

By default, the infrastructure looks for a bean named rabbitListenerContainerFactory
as the source for the factory to use to create message listener containers.
In this case, and ignoring the RabbitMQ infrastructure setup,
the processOrder(在上面的示例代码中) method can be invoked with a core poll size of three threads and a maximum pool size of ten threads.

The container factories provide methods for adding MessagePostProcessor instances
that are applied after receiving messages (before invoking the listener) and before sending replies.

you can add a RetryTemplate and RecoveryCallback to the listener container factory.
It is used when sending replies. The RecoveryCallback is invoked when retries are exhausted(耗尽).

// BaseRabbitListenerContainerFactory
factory.setRetryTemplate(retryTemplate);
factory.setReplyRecoveryCallback(ctx -> {
    Message failed = SendRetryContextAccessor.getMessage(ctx);
    Address replyTo = SendRetryContextAccessor.getAddress(ctx);
    Throwable t = ctx.getLastThrowable();
    ...
    return null;
});

The @RabbitListener annotation has a concurrency property.
It supports SpEL expressions and property placeholders.
Its meaning and allowed values depend on the container type,
Previously you had to define different container factories if you had listeners that required different concurrency.

@RabbitListener(id = "manual.acks.1", queues = "manual.acks.1", ackMode = "MANUAL")
public void manual1(String in, Channel channel,
    @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
    ...
    channel.basicAck(tag, false);
}

The annotation also allows overriding the factory autoStartup and taskExecutor properties via the autoStartup and executor annotation properties.
Using a different executor for each might help with identifying threads associated with each listener in logs and thread dumps.

added the ackMode property, which allows you to override the container factory’s acknowledgeMode property.

Message Conversion for Annotated Methods

There are two conversion steps in the pipeline before invoking the listener.

  1. The first step uses a MessageConverter to convert the incoming Spring AMQP Message to a Spring-messaging Message.
    The default MessageConverter for the first step is a Spring AMQP SimpleMessageConverter
    In the following discussion, we call this the “message converter”
  2. When the target method is invoked, the message payload is converted, if necessary, to the method parameter type.
    The default converter for the second step is a GenericMessageConverter
    In the following discussion, we call this the “method argument converter”.

To change the message converter, you can add it as a property to the container factory bean.

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    ...
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    ...
    return factory;
}

you can override the factory converter by specifying a bean name in the messageConverter property.

@Bean
public Jackson2JsonMessageConverter jsonConverter() {
    return new Jackson2JsonMessageConverter();
}

@RabbitListener(..., messageConverter = "jsonConverter")
public void listen(String in) {
    ...
}

This avoids having to declare a different container factory just to change the converter.

In most cases, it is not necessary to customize the method argument converter unless,
for example, you want to use a custom ConversionService.

In versions prior(之前的) to 1.6, the type information to convert the JSON had to be provided in message headers, or a custom ClassMapper was required.
Starting with version 1.6, if there are no type information headers, the type can be inferred(推断) from the target method arguments.

@Configuration
@EnableRabbit
public class AppConfig implements RabbitListenerConfigurer {

    ...

    @Bean
    public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
        DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
        factory.setMessageConverter(new GenericMessageConverter(myConversionService()));
        return factory;
    }

    @Bean
    public DefaultConversionService myConversionService() {
        DefaultConversionService conv = new DefaultConversionService();
        conv.addConverter(mySpecialConverter());
        return conv;
    }

    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
        registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
    }

    ...

}

For multi-method listeners (see Multi-method Listeners), the method selection is based on the payload of the message after the message conversion.
The method argument converter is called only after the method has been selected.

Adding a Custom HandlerMethodArgumentResolver to @RabbitListener

you are able to add your own HandlerMethodArgumentResolver and resolve custom method parameters.
All you need is to implement RabbitListenerConfigurer and use method setCustomMethodArgumentResolvers() from class RabbitListenerEndpointRegistrar.

@Configuration
class CustomRabbitConfig implements RabbitListenerConfigurer {

    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
        registrar.setCustomMethodArgumentResolvers(
				new HandlerMethodArgumentResolver() {

					@Override
					public boolean supportsParameter(MethodParameter parameter) {
						return CustomMethodArgument.class.isAssignableFrom(parameter.getParameterType());
					}

					@Override
					public Object resolveArgument(MethodParameter parameter, org.springframework.messaging.Message<?> message) {
						return new CustomMethodArgument(
								(String) message.getPayload(),
								message.getHeaders().get("customHeader", String.class)
						);
					}

				}
			);
    }

}

Programmatic Endpoint Registration

RabbitListenerEndpoint provides a model of a Rabbit endpoint and is responsible for configuring the container for that model.
The infrastructure lets you configure endpoints programmatically in addition to the ones that are detected by the RabbitListener annotation.

@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);
    }
}

Annotated Endpoint Method Signature

So far, we have been injecting a simple String in our endpoint, but it can actually have a very flexible method signature.

@Component
public class MyService {
    @RabbitListener(queues = "myQueue")
    public void processOrder(Order order, @Header("order_type") String orderType) {
        ...
    }
}

The following list shows the arguments that are available to be matched with parameters in listener endpoints:

  • org.springframework.amqp.core.Message
  • MessageProperties
  • com.rabbitmq.client.Channel
  • org.springframework.messaging.Message (converted from the incoming AMQP message)
  • @Header
  • @Headers (be assignable to java.util.Map), for getting access to all headers
  • The converted payload

A non-annotated element that is not one of the supported types (that is, Message, MessageProperties, Message<?> and Channel) is matched with the payload.
You can make that explicit by annotating the parameter with @Payload.
You can also turn on validation by adding an extra @Valid.

The ability to inject Spring’s message abstraction is particularly useful to benefit from all the information stored in the transport-specific message without relying on the transport-specific API.

@RabbitListener(queues = "myQueue")
public void processOrder(Message<Order> order) { ...
}

Handling of method arguments is provided by DefaultMessageHandlerMethodFactory,
which you can further customize to support additional method arguments.
The conversion and validation support can be customized there as well.

For instance, if we want to make sure our Order is valid before processing it,
we can annotate the payload with @Valid and configure the necessary validator

@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;
    }
}

@RabbitListener @Payload Validation

it is now easier to add a Validator to validate @RabbitListener and @RabbitHandler @Payload arguments.
Now, you can simply add the validator to the registrar itself.

@Configuration
@EnableRabbit
public class Config implements RabbitListenerConfigurer {
    ...
    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
      registrar.setValidator(new MyValidator());
    }
}

When using Spring Boot with the validation starter, a LocalValidatorFactoryBean is auto-configured:

@Configuration
@EnableRabbit
public class Config implements RabbitListenerConfigurer {
    @Autowired
    private LocalValidatorFactoryBean validator;
    ...
    @Override
    public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
      registrar.setValidator(this.validator);
    }
}

To validate:

@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler",
      containerFactory = "jsonListenerContainerFactory")
public void validatedListener(@Payload @Valid ValidatedClass val) {
    ...
}
@Bean
public RabbitListenerErrorHandler validationErrorHandler() {
    return (m, e) -> {
        ...
    };
}

Listening to Multiple Queues

When you use the queues attribute, you can specify that the associated container can listen to multiple queues.
You can use a @Header annotation to make the queue name from which a message was received available to the POJO method.

@RabbitListener(queues = { "queue1", "queue2" } )
public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
    ...
}

you can externalize the queue names by using property placeholders and SpEL.

@RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" )
public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) {
    ...
}

Reply Management

The existing support in MessageListenerAdapter already lets your method have a non-void return type.
When that is the case, the result of the invocation is encapsulated in a message sent to the address specified in the ReplyToAddress header of the original message,
or to the default address configured on the listener.
You can set that default address by using the @SendTo annotation of the messaging abstraction.

@RabbitListener(destination = "myQueue")
@SendTo("status")
public OrderStatus processOrder(Order order) {
    // order processing
    return status;
}

If you need to set additional headers in a transport-independent manner, you could return a Message instead,

@RabbitListener(destination = "myQueue")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
    // order processing
    return MessageBuilder
        .withPayload(status)
        .setHeader("code", 1234)
        .build();
}

Alternatively, you can use a MessagePostProcessor in the beforeSendReplyMessagePostProcessors container factory property to add more headers.
The called bean/method is made available in the reply message,
which can be used in a message post processor to communicate the information back to the caller:

factory.setBeforeSendReplyPostProcessors(msg -> {
    msg.getMessageProperties().setHeader("calledBean",
            msg.getMessageProperties().getTargetBean().getClass().getSimpleName());
    msg.getMessageProperties().setHeader("calledMethod",
            msg.getMessageProperties().getTargetMethod().getName());
    return m;
});

You can configure a ReplyPostProcessor to modify the reply message before it is sent; it is called after the correlationId header has been set up to match the request.

@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader")
public String capitalizeWithHeader(String in) {
    return in.toUpperCase();
}

@Bean
public ReplyPostProcessor echoCustomHeader() {
    return (req, resp) -> {
        resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader"));
        return resp;
    };
}

The @SendTo value is assumed as a reply exchange and routingKey pair that follows the exchange/routingKey pattern, where one of those parts can be omitted.

  • thing1/thing2: The replyTo exchange and the routingKey.
    • thing1/: The replyTo exchange and the default (empty) routingKey.
    • thing2 or /thing2: The replyTo routingKey and the default (empty) exchange.
    • / or empty: The replyTo default exchange and the default routingKey.

Also, you can use @SendTo without a value attribute. This case is equal to an empty sendTo pattern.
@SendTo is used only if the inbound message does not have a replyToAddress property.

Starting with version 1.5, the @SendTo value can be a bean initialization SpEL Expression

@RabbitListener(queues = "test.sendTo.spel")
@SendTo("#{spelReplyTo}")
public String capitalizeWithSendToSpel(String foo) {
    return foo.toUpperCase();
}
...
@Bean
public String spelReplyTo() {
    return "test.sendTo.reply.spel";
}

The #{…} expression is evaluated once, during initialization.

For dynamic reply routing, the message sender should include a reply_to message property or use the alternate runtime SpEL expression

The @SendTo can be a SpEL expression that is evaluated at runtime against the request and reply

@RabbitListener(queues = "test.sendTo.spel")
@SendTo("!{'some.reply.queue.with.' + result.queueName}")
public Bar capitalizeWithSendToSpel(Foo foo) {
    return processTheFooAndReturnABar(foo);
}

The runtime nature of the SpEL expression is indicated with !{…} delimiters. The evaluation context #root object for the expression has three properties:

  • request: : The o.s.amqp.core.Message request object
  • source: The o.s.messaging.Message<?> after conversion.
  • result: The method result.

In summary, #{…} is evaluated once during initialization, with the #root object being the application context. Beans are referenced by their names.
!{…} is evaluated at runtime for each message, with the root object having the properties listed earlier.
Beans are referenced with their names, prefixed by @.

Reply ContentType

If you are using a sophisticated(复杂) message converter, such as the ContentTypeDelegatingMessageConverter,
you can control the content type of the reply by setting the replyContentType property on the listener.

@RabbitListener(queues = "q1", messageConverter = "delegating",
        replyContentType = "application/json")
public Thing2 listen(Thing1 in) {
    ...
}

By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion.
Converters such as the SimpleMessageConverter use the reply type rather than the content type to determine the conversion needed and sets the content type in the reply message appropriately.
This may not be the desired(期望的) action and can be overridden by setting the converterWinsContentType property to false.
For example, if you return a String containing JSON, the SimpleMessageConverter will set the content type in the reply to text/plain.
The following configuration will ensure the content type is set properly, even if the SimpleMessageConverter is used.

@RabbitListener(queues = "q1", replyContentType = "application/json",
        converterWinsContentType = "false")
public String listen(Thing in) {
    ...
    return someJsonString;
}

If you wish to override that behavior, also set the AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS to true and any value set by the converter will be retained.

Multi-method Listeners

You can specify the @RabbitListener annotation at the class level.
Together with the new @RabbitHandler annotation, this lets a single listener invoke different methods, based on the payload type of the incoming message.

@RabbitListener(id="multi", queues = "someQueue")
@SendTo("my.reply.queue")
public class MultiListenerBean {

    @RabbitHandler
    public String thing2(Thing2 thing2) {
        ...
    }

    @RabbitHandler
    public String cat(Cat cat) {
        ...
    }

    @RabbitHandler
    public String hat(@Header("amqp_receivedRoutingKey") String rk, @Payload Hat hat) {
        ...
    }

    @RabbitHandler(isDefault = true)
    public String defaultMethod(Object object) {
        ...
    }

}

In this case, the individual @RabbitHandler methods are invoked if the converted payload is a Thing2, a Cat, or a Hat.
You should understand that the system must be able to identify a unique method based on the payload type.
The type is checked for assignability to a single parameter that has no annotations or that is annotated with the @Payload annotation.
Notice that the same method signatures apply, as discussed in the method-level @RabbitListener (described earlier).

A @RabbitHandler method can be designated as the default method, which is invoked if there is no match on other methods.
At most, one method can be so designated.

@RabbitHandler is intended only for processing message payloads after conversion,
if you wish to receive the unconverted raw Message object, you must use @RabbitListener on the method, not the class.

The @RabbitListener annotation is marked with @Repeatable.
This means that the annotation can appear on the same annotated element (method or class) multiple times.
In this case, a separate listener container is created for each annotation

Proxy @RabbitListener and Generics

If your service is intended to be proxied (for example, in the case of @Transactional),
you should keep in mind some considerations when the interface has generic parameters.

interface TxService<P> {

   String handle(P payload, String header);

}

static class TxServiceImpl implements TxService<Foo> {

    @Override
    @RabbitListener(...)
    public String handle(Thing thing, String rk) {
         ...
    }

}

With a generic interface and a particular implementation, you are forced to switch to the CGLIB target class proxy
because the actual implementation of the interface handle method is a bridge method. In the case of transaction management,
the use of CGLIB is configured by using an annotation option: @EnableTransactionManagement(proxyTargetClass = true).
And in this case, all annotations have to be declared on the target method in the implementation

static class TxServiceImpl implements TxService<Foo> {
    @Override
    @Transactional
    @RabbitListener(...)
    public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) {
        ...
    }
}

Handling Exceptions

By default, if an annotated listener method throws an exception, it is thrown to the container
and the message are requeued and redelivered, discarded, or routed to a dead letter exchange, depending on the container and broker configuration.
Nothing is returned to the sender.

The @RabbitListener annotation has two new attributes: errorHandler and returnExceptions.

@FunctionalInterface
public interface RabbitListenerErrorHandler {
    Object handleError(Message amqpMessage, org.springframework.messaging.Message<?> message,
              ListenerExecutionFailedException exception) throws Exception;

}

As you can see, The exception that was thrown by the listener (wrapped in a ListenerExecutionFailedException).
The error handler can either return some result (which is sent as the reply) or throw the original
or a new exception (which is thrown to the container or returned to the sender, depending on the returnExceptions setting).

The returnExceptions attribute, when true, causes exceptions to be returned to the sender. The exception is wrapped in a RemoteInvocationResult object.
On the sender side, there is an available RemoteInvocationAwareMessageConverterAdapter, which, if configured into the RabbitTemplate, re-throws the server-side exception, wrapped in an AmqpRemoteException.
The stack trace of the server exception is synthesized by merging the server and client stack traces.

This mechanism generally works only with the default SimpleMessageConverter, which uses Java serialization.
Exceptions are generally not “Jackson-friendly” and cannot be serialized to JSON.
If you use JSON, consider using an errorHandler to return some other Jackson-friendly Error object when an exception is thrown.

The Channel is available in a messaging message header;
this allows you to ack or nack the failed message when using AcknowledgeMode.MANUAL:

public Object handleError(Message amqpMessage, org.springframework.messaging.Message<?> message,
          ListenerExecutionFailedException exception) {
              ...
              message.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class)
                  .basicReject(message.getHeaders().get(AmqpHeaders.DELIVERY_TAG, Long.class),
                               true);
          }

If a message conversion exception is thrown, the error handler will be called, with null in the message argument.
This allows the application to send some result to the caller, indicating that a badly-formed message was received.

Container Management

Containers created for annotations are not registered with the application context.
You can obtain a collection of all containers by invoking getListenerContainers() on the RabbitListenerEndpointRegistry bean.

By default, stopping a container will cancel the consumer and process all prefetched messages before stopping.
You can set the [forceStop] container property to true to stop immediately after the current message is processed, causing any prefetched messages to be requeued.
This is useful, for example, if exclusive or single-active consumers are being used.

@RabbitListener with Batching

When receiving a batch of messages, the de-batching is normally performed by the container and the listener is invoked with one message at time.

You can configure the listener container factory and listener to receive the entire batch in one call,
simply set the factory’s batchListener property, and make the method payload parameter a List

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory());
    factory.setBatchListener(true);
    return factory;
}

@RabbitListener(queues = "batch.1")
public void listen1(List<Thing> in) {
    ...
}

// or

@RabbitListener(queues = "batch.2")
public void listen2(List<Message<Thing>> in) {
    ...
}

Setting the batchListener property to true automatically turns off the deBatchingEnabled container property in containers
that the factory creates (unless consumerBatchEnabled is true - see below).
Effectively, the debatching is moved from the container to the listener adapter and the adapter creates the list that is passed to the listener.

A batch-enabled factory cannot be used with a multi-method listener.

When receiving batched messages one-at-a-time, the last message contains a boolean header set to true.
This header can be obtained by adding the @Header(AmqpHeaders.LAST_IN_BATCH) boolean last parameter to your listener method.
The header is mapped from MessageProperties.isLastInBatch().
In addition, AmqpHeaders.BATCH_SIZE is populated with the size of the batch in every message fragment.

In addition, a new property consumerBatchEnabled has been added to the SimpleMessageListenerContainer.
When this is true, the container will create a batch of messages, up to batchSize;
A partial batch is delivered if receiveTimeout elapses(流逝) with no new messages arriving.
If a producer-created batch is received, it is debatched and added to the consumer-side batch;
Therefore the actual number of messages delivered may exceed batchSize,
which represents the number of messages received from the broker.
deBatchingEnabled must be true when consumerBatchEnabled is true;
The container factory will enforce this requirement.

@Bean
public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(rabbitConnectionFactory());
    factory.setConsumerTagStrategy(consumerTagStrategy());
    factory.setBatchListener(true); // configures a BatchMessageListenerAdapter
    factory.setBatchSize(2);
    factory.setConsumerBatchEnabled(true);
    return factory;
}

When using consumerBatchEnabled with @RabbitListener:

@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory")
public void consumerBatch1(List<Message> amqpMessages) {
    this.amqpMessagesReceived = amqpMessages;
    this.batch1Latch.countDown();
}

@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory")
public void consumerBatch2(List<org.springframework.messaging.Message<Invoice>> messages) {
    this.messagingMessagesReceived = messages;
    this.batch2Latch.countDown();
}

@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory")
public void consumerBatch3(List<Invoice> strings) {
    this.batch3Strings = strings;
    this.batch3Latch.countDown();
}

You can also add a Channel parameter, often used when using MANUAL ack mode.
This is not very useful with the third example because you don’t have access to the delivery_tag property.

Using Container Factories

Listener container factories were introduced to support the @RabbitListenerand registering containers with the RabbitListenerEndpointRegistry

they can be used to create any listener container —— even a container without a listener (such as for use in Spring Integration).
Of course, a listener must be added before the container is started.

There are two ways to create such containers:

  • Use a SimpleRabbitListenerEndpoint
@Bean
public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener(
        SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) {
    SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
    endpoint.setQueueNames("queue.1");
    endpoint.setMessageListener(message -> {
        ...
    });
    return rabbitListenerContainerFactory.createListenerContainer(endpoint);
}
  • Add the listener after creation
@Bean
public SimpleMessageListenerContainer factoryCreatedContainerNoListener(
        SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) {
    SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer();
    container.setMessageListener(message -> {
        ...
    });
    container.setQueueNames("test.no.listener.yet");
    return container;
}

In either case(无论哪些情况), the listener can also be a ChannelAwareMessageListener, since it is now a sub-interface of MessageListener.

Containers created this way are normal @Bean instances and are not registered in the RabbitListenerEndpointRegistry.

Asynchronous @RabbitListener Return Types

@RabbitListener (and @RabbitHandler) methods can be specified with asynchronous return types ListenableFuture and Mono, letting the reply be sent asynchronously.

The listener container factory must be configured with AcknowledgeMode.MANUAL so that the consumer thread will not ack the message;
instead, the asynchronous completion will ack or nack the message when the async operation completes.

When the async result is completed with an error, whether the message is requeued or not depends on the exception type thrown, the container configuration, and the container error handler.
By default, the message will be requeued, unless the container’s defaultRequeueRejected property is set to false. (it is true by default).

If the async result is completed with an AmqpRejectAndDontRequeueException, the message will not be requeued.
If the container’s defaultRequeueRejected property is false, you can override that by setting the future’s exception to a ImmediateRequeueException and the message will be requeued.
If some exception occurs within the listener method that prevents creation of the async result object, you MUST catch that exception and return an appropriate return object that will cause the message to be acknowledged or requeued.

Starting with versions 2.2.21, 2.3.13, 2.4.1, the AcknowledgeMode will be automatically set the MANUAL when async return types are detected.
In addition, incoming messages with fatal exceptions will be negatively acknowledged individually,
previously any prior unacknowledged message were also negatively acknowledged.

Threading and Asynchronous Consumers

  • Threads from the TaskExecutor configured in the SimpleMessageListenerContainer are used to invoke the MessageListener when a new message is delivered by RabbitMQ Client. If not configured, a SimpleAsyncTaskExecutor is used. If you use a pooled executor, you need to ensure the pool size is sufficient to handle the configured concurrency.

It is recommended that you use a similar technique to name the threads created by a custom TaskExecutor bean definition, to aid with thread identification in log messages.

  • With the DirectMessageListenerContainer, the MessageListener is invoked directly on a RabbitMQ Client thread. In this case, the taskExecutor is used for the task that monitors the consumers.

With the DirectMessageListenerContainer, you need to ensure that the connection factory is configured with a task executor that has sufficient threads to support your desired concurrency across all listener containers that use that factory.

The RabbitMQ client uses a ThreadFactory to create threads for low-level I/O (socket) operations. To modify this factory, you need to configure the underlying RabbitMQ ConnectionFactory


  • The Executor configured in the CachingConnectionFactory is passed into the RabbitMQ Client when creating the connection, and its threads are used to deliver new messages to the listener container.

If you have a large number of factories or are using CacheMode.CONNECTION, you may wish to consider using a shared ThreadPoolTaskExecutor with enough threads to satisfy your workload.

Choosing a Container

The SMLC(SimpleMessageListenerContainer) uses an internal queue and a dedicated(专门的) thread for each consumer.

  • If a container is configured to listen to multiple queues, the same consumer thread is used to process all the queues.
  • Concurrency is controlled by concurrentConsumers and other properties.

As messages arrive from the RabbitMQ client, the client thread hands them off(交给它们) to the consumer thread through the queue.
This architecture was required because,
in early versions of the RabbitMQ client, multiple concurrent deliveries were not possible.
Newer versions of the client have a revised(修订的) threading model and can now support concurrency.

This has allowed the introduction of the DMLC(DirectMessageListenerContainer)
where the listener is now invoked directly on the RabbitMQ Client thread.
Its architecture is, therefore, actually “simpler” than the SMLC.
However, there are some limitations with this approach, and certain features of the SMLC are not available with the DMLC.

  • Also, concurrency is controlled by consumersPerQueue (and the client library’s thread pool).
    The concurrentConsumers and associated properties are not available with this container.

The following features are available with the SMLC but not the DMLC:

  • batchSize: With the SMLC, you can set this to control how many messages are delivered in a transaction or to reduce the number of acks
    , but it may cause the number of duplicate deliveries to increase after a failure. (The DMLC does have messagesPerAck
    , which you can use to reduce the acks, the same as with batchSize and the SMLC, but it cannot be used with transactions —— each message is delivered and ack’d in a separate transaction).
  • consumerBatchEnabled: enables batching of discrete messages in the consumer; see Message Listener Container Configuration for more information.
  • maxConcurrentConsumers and consumer scaling intervals or triggers —— there is no auto-scaling in the DMLC.
    It does, however, let you programmatically change the consumersPerQueue property and the consumers are adjusted accordingly.

However, the DMLC has the following benefits over the SMLC:

  • Adding and removing queues at runtime is more efficient. With the SMLC, the entire consumer thread is restarted (all consumers canceled and re-created).
    With the DMLC, unaffected consumers are not canceled.
  • The context switch between the RabbitMQ Client thread and the consumer thread is avoided.

Detecting Idle Asynchronous Consumers

While efficient, one problem with asynchronous consumers is detecting when they are idle ——
users might want to take some action if no messages arrive for some period of time.

it is now possible to configure the listener container to publish a ListenerContainerIdleEvent when some time passes with no message delivery.
While the container is idle, an event is published every idleEventInterval milliseconds.

@Bean
public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
    ...
    container.setIdleEventInterval(60000L);
    ...
    return container;
}

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(rabbitConnectionFactory());
    factory.setIdleEventInterval(60000L);
    ...
    return factory;
}

Event Consumption(消费)

You need to understand that the application listener gets events for all containers,
so you may need to check the listener ID if you want to take specific action based on which container is idle.
You can also use the @EventListener condition for this purpose.

The events have four properties:

  • source: The listener container instance
  • id: The listener ID (or container bean name)
  • idleTime: The time the container had been idle when the event was published
  • queueNames: The names of the queue(s) that the container listens to
public class Listener {

    @RabbitListener(id="someId", queues="#{queue.name}")
    public String listen(String foo) {
        return foo.toUpperCase();
    }

    @EventListener(condition = "event.listenerId == 'someId'")
    public void onApplicationEvent(ListenerContainerIdleEvent event) {
        ...
    }

}

Event listeners see events for all containers. Consequently, in the preceding example, we narrow the events received based on the listener ID.

If you wish to use the idle event to stop the lister container, you should not call container.stop() on the thread
that calls the listener. Doing so always causes delays and unnecessary log messages.
Instead, you should hand off the event to a different thread that can then stop the container.

Monitoring Listener Performance

the listener containers will automatically create and update Micrometer Timer s for the listener

Two timers are maintained - one for successful calls to the listener and one for failures.

4.1.7. Containers and Broker-Named queues

While(虽然) it is preferable to use AnonymousQueue instances as auto-delete queues,
you can use broker named queues with listener containers.

@Bean
public Queue queue() {
    return new Queue("", false, true, true);
}

@Bean
public SimpleMessageListenerContainer container() {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf());
    container.setQueues(queue());
    container.setMessageListener(m -> {
        ...
    });
    container.setMissingQueuesFatal(false);
    return container;
}

Notice the empty String for the name. When the RabbitAdmin declares queues, it updates the Queue.actualName property with the name returned by the broker.
You must use setQueues() when you configure the container for this to work,
so that the container can access the declared name at runtime. Just setting the names is insufficient(不足的).

You cannot add broker-named queues to the containers while they are running.

When a connection is reset and a new one is established, the new queue gets a new name.
Since there is a race(竞争) condition between the container restarting and the queue being re-declared,
it is important to set the container’s missingQueuesFatal property to false,
since the container is likely to initially try to reconnect to the old queue.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值