GH-1489: Batch RabbitListener Improvements
Resolves https://github.com/spring-projects/spring-amqp/issues/1489 - allow listeners to consume `Collection<?>` as well as `List<?>` - detect a non-batch listener method in the batch adapter - coerce `batchListener` to `true` when `consumerBatchEnabled` is true
This commit is contained in:
committed by
Artem Bilan
parent
515eb9a32b
commit
d96aa71ec1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2020 the original author or authors.
|
||||
* Copyright 2014-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -123,13 +123,18 @@ public class SimpleRabbitListenerContainerFactory
|
||||
|
||||
/**
|
||||
* Set to true to present a list of messages based on the {@link #setBatchSize(Integer)},
|
||||
* if the listener supports it.
|
||||
* if the listener supports it. Starting with version 3.0, setting this to true will
|
||||
* also {@link #setBatchListener(boolean)} to true.
|
||||
* @param consumerBatchEnabled true to create message batches in the container.
|
||||
* @since 2.2
|
||||
* @see #setBatchSize(Integer)
|
||||
* @see #setBatchListener(boolean)
|
||||
*/
|
||||
public void setConsumerBatchEnabled(boolean consumerBatchEnabled) {
|
||||
this.consumerBatchEnabled = consumerBatchEnabled;
|
||||
if (consumerBatchEnabled) {
|
||||
setBatchListener(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
@@ -328,6 +329,8 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
|
||||
private boolean isAmqpMessageList;
|
||||
|
||||
private boolean isCollection;
|
||||
|
||||
MessagingMessageConverterAdapter(Object bean, Method method, boolean batch) {
|
||||
this.bean = bean;
|
||||
this.method = method;
|
||||
@@ -392,6 +395,12 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
|
||||
if (genericParameterType == null) {
|
||||
genericParameterType = extractGenericParameterTypFromMethodParameter(methodParameter);
|
||||
if (this.isBatch && !this.isCollection) {
|
||||
throw new IllegalStateException(
|
||||
"Mis-configuration; a batch listener must consume a List<?> or "
|
||||
+ "Collection<?> for method: " + this.method);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
if (MessagingMessageListenerAdapter.this.logger.isDebugEnabled()) {
|
||||
@@ -435,9 +444,11 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0];
|
||||
}
|
||||
else if (this.isBatch
|
||||
&& parameterizedType.getRawType().equals(List.class)
|
||||
&& parameterizedType.getActualTypeArguments().length == 1) {
|
||||
&& ((parameterizedType.getRawType().equals(List.class)
|
||||
|| parameterizedType.getRawType().equals(Collection.class))
|
||||
&& parameterizedType.getActualTypeArguments().length == 1)) {
|
||||
|
||||
this.isCollection = true;
|
||||
Type paramType = parameterizedType.getActualTypeArguments()[0];
|
||||
boolean messageHasGeneric = paramType instanceof ParameterizedType
|
||||
&& ((ParameterizedType) paramType).getRawType().equals(Message.class);
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.springframework.amqp.rabbit.annotation;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -51,7 +53,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
*/
|
||||
@SpringJUnitConfig
|
||||
@DirtiesContext
|
||||
@RabbitAvailable(queues = { "batch.1", "batch.2", "batch.3", "batch.4" })
|
||||
@RabbitAvailable(queues = { "batch.1", "batch.2", "batch.3", "batch.4", "batch.5" })
|
||||
public class EnableRabbitBatchIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
@@ -114,6 +116,16 @@ public class EnableRabbitBatchIntegrationTests {
|
||||
.isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collectionWithStringInfer() throws InterruptedException {
|
||||
this.template.convertAndSend("batch.5", new Foo("foo"));
|
||||
this.template.convertAndSend("batch.5", new Foo("bar"));
|
||||
assertThat(this.listener.fivesLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(this.listener.fives).hasSize(2);
|
||||
assertThat(this.listener.fives.get(0).getBar()).isEqualTo("foo");
|
||||
assertThat(this.listener.fives.get(1).getBar()).isEqualTo("bar");
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRabbit
|
||||
public static class Config {
|
||||
@@ -186,6 +198,10 @@ public class EnableRabbitBatchIntegrationTests {
|
||||
|
||||
CountDownLatch fooConsumerBatchTooLatch = new CountDownLatch(1);
|
||||
|
||||
List<Foo> fives = new ArrayList<>();
|
||||
|
||||
CountDownLatch fivesLatch = new CountDownLatch(1);
|
||||
|
||||
private List<org.springframework.amqp.core.Message> nativeMessages;
|
||||
|
||||
private final CountDownLatch nativeMessagesLatch = new CountDownLatch(1);
|
||||
@@ -214,6 +230,12 @@ public class EnableRabbitBatchIntegrationTests {
|
||||
this.nativeMessagesLatch.countDown();
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "batch.5")
|
||||
public void listen5(Collection<Foo> in) {
|
||||
this.fives.addAll(in);
|
||||
this.fivesLatch.countDown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.amqp.rabbit.listener.adapter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.amqp.utils.test.TestUtils;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 3.0
|
||||
*
|
||||
*/
|
||||
public class BatchMessagingMessageListenerAdapterTests {
|
||||
|
||||
@Test
|
||||
void compatibleMethod() throws Exception {
|
||||
Method method = getClass().getDeclaredMethod("listen", List.class);
|
||||
BatchMessagingMessageListenerAdapter adapter = new BatchMessagingMessageListenerAdapter(this, method, false,
|
||||
null, null);
|
||||
assertThat(TestUtils.getPropertyValue(adapter, "messagingMessageConverter.inferredArgumentType"))
|
||||
.isEqualTo(String.class);
|
||||
Method badMethod = getClass().getDeclaredMethod("listen", String.class);
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new BatchMessagingMessageListenerAdapter(this, badMethod, false, null, null)
|
||||
).withMessageStartingWith("Mis-configuration");
|
||||
}
|
||||
|
||||
public void listen(String in) {
|
||||
}
|
||||
|
||||
public void listen(List<String> in) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3422,7 +3422,7 @@ Adding a `group` attribute causes a bean of type `Collection<MessageListenerCont
|
||||
===== @RabbitListener with Batching
|
||||
|
||||
When receiving a <<template-batching, a batch>> of messages, the de-batching is normally performed by the container and the listener is invoked with one message at at time.
|
||||
Starting with version 2.2, 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`:
|
||||
Starting with version 2.2, 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` or `Collection`:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
@@ -3486,20 +3486,17 @@ 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();
|
||||
...
|
||||
}
|
||||
----
|
||||
====
|
||||
@@ -3511,6 +3508,12 @@ public void consumerBatch3(List<Invoice> strings) {
|
||||
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.
|
||||
|
||||
Spring Boot provides a configuration property for `consumerBatchEnabled` and `batchSize`, but not for `batchListener`.
|
||||
Starting with version 3.0, setting `consumerBatchEnabled` to `true` on the container factory also sets `batchListener` to `true`.
|
||||
When `consumerBatchEnabled` is `true`, the listener **must** be a batch listener.
|
||||
|
||||
Starting with version 3.0, listener methods can consume `Collection<?>` or `List<?>`.
|
||||
|
||||
[[using-container-factories]]
|
||||
===== Using Container Factories
|
||||
|
||||
|
||||
@@ -21,3 +21,10 @@ See <<async-template>> for more information.
|
||||
|
||||
`RabbitStreamOperations2` and `RabbitStreamTemplate2` have been deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively.
|
||||
See <<stream-support>> for more information.
|
||||
|
||||
==== `@RabbitListener` Changes
|
||||
|
||||
Batch listeners can now consume `Collection<?>` as well as `List<?>`.
|
||||
The batch messaging adapter now ensures that the method is suitable for consuming batches.
|
||||
When setting the container factory `consumerBatchEnabled` to `true`, the `batchListener` property is also set to `true`.
|
||||
See <<receiving-batch>> for more infoprmation.
|
||||
|
||||
Reference in New Issue
Block a user